diff --git a/.eslintrc.json b/.eslintrc.json index ba382f622ac..e1dc514e65e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -183,6 +183,7 @@ "sinon", "vs/nls", "**/vs/base/common/**", + "**/vs/base/parts/*/common/**", "**/vs/platform/*/common/**", "**/vs/platform/*/test/common/**" ] diff --git a/.vscode/launch.json b/.vscode/launch.json index d0e6ded3f5b..caaed4c25a7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,7 +16,6 @@ "request": "attach", "name": "Attach to Extension Host", "port": 5870, - "restart": true, "outFiles": [ "${workspaceFolder}/out/**/*.js" ] diff --git a/README.md b/README.md index 0de5f2f71da..1d3bc28f9bb 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ please see the document [How to Contribute](https://github.com/Microsoft/vscode/ * Ask a question on [Stack Overflow](https://stackoverflow.com/questions/tagged/vscode) * [Request a new feature](CONTRIBUTING.md) -* Up vote [popular feature requests](https://github.com/Microsoft/vscode/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request+sort%3Areactions-%2B1-desc) +* Upvote [popular feature requests](https://github.com/Microsoft/vscode/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request+sort%3Areactions-%2B1-desc) * [File an issue](https://github.com/Microsoft/vscode/issues) * Follow [@code](https://twitter.com/code) and let us know what you think! diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index 5960fa7fda1..eeacf8c78bc 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -46,7 +46,7 @@ }, { "name": "ms-vscode.js-debug-nightly", - "version": "2020.2.1017", + "version": "2020.2.1317", "forQualities": [ "insider" ], diff --git a/extensions/git/package.json b/extensions/git/package.json index c1cd8b5135c..dcfdcd2171c 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 f3d75519653..53325c0f5f7 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 dba21595ea8..9d8fe2a4ef4 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 7bb5081d12f..ddf16d76aac 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 3aa36a0dccb..9db3495aec7 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 af5b10127dc..ac135e43a80 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/grunt/src/main.ts b/extensions/grunt/src/main.ts index d8ef8a82673..597aa909701 100644 --- a/extensions/grunt/src/main.ts +++ b/extensions/grunt/src/main.ts @@ -295,7 +295,7 @@ class TaskDetector { private updateProvider(): void { if (!this.taskProvider && this.detectors.size > 0) { const thisCapture = this; - this.taskProvider = vscode.workspace.registerTaskProvider('grunt', { + this.taskProvider = vscode.tasks.registerTaskProvider('grunt', { provideTasks: (): Promise => { return thisCapture.getTasks(); }, diff --git a/extensions/gulp/src/main.ts b/extensions/gulp/src/main.ts index 20d824c25ed..534756ac81d 100644 --- a/extensions/gulp/src/main.ts +++ b/extensions/gulp/src/main.ts @@ -277,7 +277,7 @@ class TaskDetector { private updateProvider(): void { if (!this.taskProvider && this.detectors.size > 0) { const thisCapture = this; - this.taskProvider = vscode.workspace.registerTaskProvider('gulp', { + this.taskProvider = vscode.tasks.registerTaskProvider('gulp', { provideTasks(): Promise { return thisCapture.getTasks(); }, diff --git a/extensions/html-language-features/server/src/htmlServerMain.ts b/extensions/html-language-features/server/src/htmlServerMain.ts index 7f0045fef14..9ed3ce0f0a0 100644 --- a/extensions/html-language-features/server/src/htmlServerMain.ts +++ b/extensions/html-language-features/server/src/htmlServerMain.ts @@ -515,22 +515,6 @@ connection.onRequest(MatchingTagPositionRequest.type, (params, token) => { }, null, `Error while computing matching tag position for ${params.textDocument.uri}`, token); }); -connection.onRequest(MatchingTagPositionRequest.type, (params, token) => { - return runSafe(() => { - const document = documents.get(params.textDocument.uri); - if (document) { - const pos = params.position; - if (pos.character > 0) { - const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1)); - if (mode && mode.findMatchingTagPosition) { - return mode.findMatchingTagPosition(document, pos); - } - } - } - return null; - }, null, `Error while computing matching tag position for ${params.textDocument.uri}`, token); -}); - let semanticTokensProvider: SemanticTokenProvider | undefined; function getSemanticTokenProvider() { if (!semanticTokensProvider) { diff --git a/extensions/jake/src/main.ts b/extensions/jake/src/main.ts index b8c6d5e7344..93fa1aa285b 100644 --- a/extensions/jake/src/main.ts +++ b/extensions/jake/src/main.ts @@ -269,7 +269,7 @@ class TaskDetector { private updateProvider(): void { if (!this.taskProvider && this.detectors.size > 0) { const thisCapture = this; - this.taskProvider = vscode.workspace.registerTaskProvider('jake', { + this.taskProvider = vscode.tasks.registerTaskProvider('jake', { provideTasks(): Promise { return thisCapture.getTasks(); }, diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 0c26777f01f..a21b05e4aba 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -1,7 +1,7 @@ { "name": "vscode-json-languageserver", "description": "JSON language server", - "version": "1.2.2", + "version": "1.2.3", "author": "Microsoft Corporation", "license": "MIT", "engines": { diff --git a/extensions/npm/src/main.ts b/extensions/npm/src/main.ts index 3777f393757..4092e27e662 100644 --- a/extensions/npm/src/main.ts +++ b/extensions/npm/src/main.ts @@ -70,7 +70,7 @@ function registerTaskProvider(context: vscode.ExtensionContext): vscode.Disposab context.subscriptions.push(workspaceWatcher); let provider: vscode.TaskProvider = new NpmTaskProvider(); - let disposable = vscode.workspace.registerTaskProvider('npm', provider); + let disposable = vscode.tasks.registerTaskProvider('npm', provider); context.subscriptions.push(disposable); return disposable; } diff --git a/extensions/php-language-features/src/features/validationProvider.ts b/extensions/php-language-features/src/features/validationProvider.ts index 122c108ac37..5c9d34afb59 100644 --- a/extensions/php-language-features/src/features/validationProvider.ts +++ b/extensions/php-language-features/src/features/validationProvider.ts @@ -247,7 +247,7 @@ export default class PHPValidationProvider { } }; - let options = vscode.workspace.rootPath ? { cwd: vscode.workspace.rootPath } : undefined; + let options = (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0]) ? { cwd: vscode.workspace.workspaceFolders[0].uri.fsPath } : undefined; let args: string[]; if (this.trigger === RunTrigger.onSave) { args = PHPValidationProvider.FileArgs.slice(0); diff --git a/extensions/search-result/syntaxes/generateTMLanguage.js b/extensions/search-result/syntaxes/generateTMLanguage.js index ce2acc34ff6..7a9eb9204f6 100644 --- a/extensions/search-result/syntaxes/generateTMLanguage.js +++ b/extensions/search-result/syntaxes/generateTMLanguage.js @@ -1,6 +1,4 @@ // @ts-check -// todo@jackson -/* eslint code-no-unexternalized-strings: 0 */ const mappings = [ ['bat', 'source.batchfile'], @@ -40,6 +38,7 @@ const mappings = [ ['perl', 'source.perl'], ['php', 'source.php'], ['pl', 'source.perl'], + ['pm', 'source.perl'], ['ps1', 'source.powershell'], ['pug', 'text.pug'], ['py', 'source.python'], @@ -104,43 +103,43 @@ mappings.forEach(([ext, scope, regexp]) => repository[ext] = { name: scopes.resultBlock.meta, begin: `^(?!\\s)(.*?)([^\\\\\\/\\n]*${regexp || `\\.${ext}`})(:)$`, - end: "^(?!\\s)", + end: '^(?!\\s)', beginCaptures: { - "0": { name: scopes.resultBlock.path.meta }, - "1": { name: scopes.resultBlock.path.dirname }, - "2": { name: scopes.resultBlock.path.basename }, - "3": { name: scopes.resultBlock.path.colon }, + '0': { name: scopes.resultBlock.path.meta }, + '1': { name: scopes.resultBlock.path.dirname }, + '2': { name: scopes.resultBlock.path.basename }, + '3': { name: scopes.resultBlock.path.colon }, }, patterns: [ { name: [scopes.resultBlock.result.meta, scopes.resultBlock.result.metaMultiLine].join(' '), - begin: "^ ((\\d+) )", - while: "^ (?:((\\d+)(:))|((\\d+) ))", + begin: '^ ((\\d+) )', + while: '^ (?:((\\d+)(:))|((\\d+) ))', beginCaptures: { - "0": { name: scopes.resultBlock.result.prefix.meta }, - "1": { name: scopes.resultBlock.result.prefix.metaContext }, - "2": { name: scopes.resultBlock.result.prefix.lineNumber }, + '0': { name: scopes.resultBlock.result.prefix.meta }, + '1': { name: scopes.resultBlock.result.prefix.metaContext }, + '2': { name: scopes.resultBlock.result.prefix.lineNumber }, }, whileCaptures: { - "0": { name: scopes.resultBlock.result.prefix.meta }, - "1": { name: scopes.resultBlock.result.prefix.metaMatch }, - "2": { name: scopes.resultBlock.result.prefix.lineNumber }, - "3": { name: scopes.resultBlock.result.prefix.colon }, + '0': { name: scopes.resultBlock.result.prefix.meta }, + '1': { name: scopes.resultBlock.result.prefix.metaMatch }, + '2': { name: scopes.resultBlock.result.prefix.lineNumber }, + '3': { name: scopes.resultBlock.result.prefix.colon }, - "4": { name: scopes.resultBlock.result.prefix.metaContext }, - "5": { name: scopes.resultBlock.result.prefix.lineNumber }, + '4': { name: scopes.resultBlock.result.prefix.metaContext }, + '5': { name: scopes.resultBlock.result.prefix.lineNumber }, }, patterns: [{ include: scope }] }, { - begin: "^ ((\\d+)(:))", - while: "(?=not)possible", + begin: '^ ((\\d+)(:))', + while: '(?=not)possible', name: [scopes.resultBlock.result.meta, scopes.resultBlock.result.metaSingleLine].join(' '), beginCaptures: { - "0": { name: scopes.resultBlock.result.prefix.meta }, - "1": { name: scopes.resultBlock.result.prefix.metaMatch }, - "2": { name: scopes.resultBlock.result.prefix.lineNumber }, - "3": { name: scopes.resultBlock.result.prefix.colon }, + '0': { name: scopes.resultBlock.result.prefix.meta }, + '1': { name: scopes.resultBlock.result.prefix.metaMatch }, + '2': { name: scopes.resultBlock.result.prefix.lineNumber }, + '3': { name: scopes.resultBlock.result.prefix.colon }, }, patterns: [{ include: scope }] } @@ -149,10 +148,10 @@ mappings.forEach(([ext, scope, regexp]) => const header = [ { - begin: "^(# Query): ", - end: "\n", + begin: '^(# Query): ', + end: '\n', name: scopes.header.meta, - beginCaptures: { "1": { name: scopes.header.key }, }, + beginCaptures: { '1': { name: scopes.header.key }, }, patterns: [ { match: '(\\\\n)|(\\\\\\\\)', @@ -169,10 +168,10 @@ const header = [ ] }, { - begin: "^(# Flags): ", - end: "\n", + begin: '^(# Flags): ', + end: '\n', name: scopes.header.meta, - beginCaptures: { "1": { name: scopes.header.key }, }, + beginCaptures: { '1': { name: scopes.header.key }, }, patterns: [ { match: '(RegExp|CaseSensitive|IgnoreExcludeSettings|WordMatch)', @@ -182,10 +181,10 @@ const header = [ ] }, { - begin: "^(# ContextLines): ", - end: "\n", + begin: '^(# ContextLines): ', + end: '\n', name: scopes.header.meta, - beginCaptures: { "1": { name: scopes.header.key }, }, + beginCaptures: { '1': { name: scopes.header.key }, }, patterns: [ { match: '\\d', @@ -195,42 +194,42 @@ const header = [ ] }, { - match: "^(# (?:Including|Excluding)): (.*)$", + match: '^(# (?:Including|Excluding)): (.*)$', name: scopes.header.meta, captures: { - "1": { name: scopes.header.key }, - "2": { name: scopes.header.value } + '1': { name: scopes.header.key }, + '2': { name: scopes.header.value } } }, ]; const plainText = [ { - match: "^(?!\\s)(.*?)([^\\\\\\/\\n]*)(:)$", + match: '^(?!\\s)(.*?)([^\\\\\\/\\n]*)(:)$', name: [scopes.resultBlock.meta, scopes.resultBlock.path.meta].join(' '), captures: { - "1": { name: scopes.resultBlock.path.dirname }, - "2": { name: scopes.resultBlock.path.basename }, - "3": { name: scopes.resultBlock.path.colon } + '1': { name: scopes.resultBlock.path.dirname }, + '2': { name: scopes.resultBlock.path.basename }, + '3': { name: scopes.resultBlock.path.colon } } }, { - match: "^ (?:((\\d+)(:))|((\\d+)( ))(.*))", + match: '^ (?:((\\d+)(:))|((\\d+)( ))(.*))', name: [scopes.resultBlock.meta, scopes.resultBlock.result.meta].join(' '), captures: { - "1": { name: [scopes.resultBlock.result.prefix.meta, scopes.resultBlock.result.prefix.metaMatch].join(' ') }, - "2": { name: scopes.resultBlock.result.prefix.lineNumber }, - "3": { name: scopes.resultBlock.result.prefix.colon }, + '1': { name: [scopes.resultBlock.result.prefix.meta, scopes.resultBlock.result.prefix.metaMatch].join(' ') }, + '2': { name: scopes.resultBlock.result.prefix.lineNumber }, + '3': { name: scopes.resultBlock.result.prefix.colon }, - "4": { name: [scopes.resultBlock.result.prefix.meta, scopes.resultBlock.result.prefix.metaContext].join(' ') }, - "5": { name: scopes.resultBlock.result.prefix.lineNumber }, + '4': { name: [scopes.resultBlock.result.prefix.meta, scopes.resultBlock.result.prefix.metaContext].join(' ') }, + '5': { name: scopes.resultBlock.result.prefix.lineNumber }, } } ]; const tmLanguage = { - "information_for_contributors": "This file is generated from ./generateTMLanguage.js.", - name: "Search Results", + 'information_for_contributors': 'This file is generated from ./generateTMLanguage.js.', + name: 'Search Results', scopeName: scopes.root, patterns: [ ...header, diff --git a/extensions/search-result/syntaxes/searchResult.tmLanguage.json b/extensions/search-result/syntaxes/searchResult.tmLanguage.json index ea4e3efb154..8edb4a7f89a 100644 --- a/extensions/search-result/syntaxes/searchResult.tmLanguage.json +++ b/extensions/search-result/syntaxes/searchResult.tmLanguage.json @@ -189,6 +189,9 @@ { "include": "#pl" }, + { + "include": "#pm" + }, { "include": "#ps1" }, @@ -3457,6 +3460,92 @@ } ] }, + "pm": { + "name": "meta.resultBlock.search", + "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.pm)(:)$", + "end": "^(?!\\s)", + "beginCaptures": { + "0": { + "name": "string meta.path.search" + }, + "1": { + "name": "meta.path.dirname.search" + }, + "2": { + "name": "meta.path.basename.search" + }, + "3": { + "name": "punctuation.separator" + } + }, + "patterns": [ + { + "name": "meta.resultLine.search meta.resultLine.multiLine.search", + "begin": "^ ((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", + "beginCaptures": { + "0": { + "name": "constant.numeric.integer meta.resultLinePrefix.search" + }, + "1": { + "name": "meta.resultLinePrefix.contextLinePrefix.search" + }, + "2": { + "name": "meta.resultLinePrefix.lineNumber.search" + } + }, + "whileCaptures": { + "0": { + "name": "constant.numeric.integer meta.resultLinePrefix.search" + }, + "1": { + "name": "meta.resultLinePrefix.matchLinePrefix.search" + }, + "2": { + "name": "meta.resultLinePrefix.lineNumber.search" + }, + "3": { + "name": "punctuation.separator" + }, + "4": { + "name": "meta.resultLinePrefix.contextLinePrefix.search" + }, + "5": { + "name": "meta.resultLinePrefix.lineNumber.search" + } + }, + "patterns": [ + { + "include": "source.perl" + } + ] + }, + { + "begin": "^ ((\\d+)(:))", + "while": "(?=not)possible", + "name": "meta.resultLine.search meta.resultLine.singleLine.search", + "beginCaptures": { + "0": { + "name": "constant.numeric.integer meta.resultLinePrefix.search" + }, + "1": { + "name": "meta.resultLinePrefix.matchLinePrefix.search" + }, + "2": { + "name": "meta.resultLinePrefix.lineNumber.search" + }, + "3": { + "name": "punctuation.separator" + } + }, + "patterns": [ + { + "include": "source.perl" + } + ] + } + ] + }, "ps1": { "name": "meta.resultBlock.search", "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.ps1)(:)$", diff --git a/extensions/typescript-language-features/src/extension.ts b/extensions/typescript-language-features/src/extension.ts index 7416c5efdc3..d9f8284fd24 100644 --- a/extensions/typescript-language-features/src/extension.ts +++ b/extensions/typescript-language-features/src/extension.ts @@ -38,7 +38,7 @@ export function activate( }); registerCommands(commandManager, lazyClientHost, pluginManager); - context.subscriptions.push(vscode.workspace.registerTaskProvider('typescript', new TscTaskProvider(lazyClientHost.map(x => x.serviceClient)))); + context.subscriptions.push(vscode.tasks.registerTaskProvider('typescript', new TscTaskProvider(lazyClientHost.map(x => x.serviceClient)))); context.subscriptions.push(new LanguageConfigurationManager()); import('./features/tsconfig').then(module => { diff --git a/extensions/typescript-language-features/src/features/documentSymbol.ts b/extensions/typescript-language-features/src/features/documentSymbol.ts index 866a86bfebc..02c1be917d3 100644 --- a/extensions/typescript-language-features/src/features/documentSymbol.ts +++ b/extensions/typescript-language-features/src/features/documentSymbol.ts @@ -32,6 +32,7 @@ const getSymbolKind = (kind: string): vscode.SymbolKind => { }; class TypeScriptDocumentSymbolProvider implements vscode.DocumentSymbolProvider { + public constructor( private readonly client: ITypeScriptServiceClient, private cachedResponse: CachedResponse, @@ -45,23 +46,27 @@ class TypeScriptDocumentSymbolProvider implements vscode.DocumentSymbolProvider const args: Proto.FileRequestArgs = { file }; const response = await this.cachedResponse.execute(document, () => this.client.execute('navtree', args, token)); - if (response.type !== 'response' || !response.body) { + if (response.type !== 'response' || !response.body?.childItems) { return undefined; } - let tree = response.body; - if (tree && tree.childItems) { - // The root represents the file. Ignore this when showing in the UI - const result: vscode.DocumentSymbol[] = []; - tree.childItems.forEach(item => TypeScriptDocumentSymbolProvider.convertNavTree(document.uri, result, item)); - return result; + // The root represents the file. Ignore this when showing in the UI + const result: vscode.DocumentSymbol[] = []; + for (const item of response.body.childItems) { + TypeScriptDocumentSymbolProvider.convertNavTree(document.uri, result, item); } - - return undefined; + return result; } - private static convertNavTree(resource: vscode.Uri, bucket: vscode.DocumentSymbol[], item: Proto.NavigationTree): boolean { + private static convertNavTree( + resource: vscode.Uri, + output: vscode.DocumentSymbol[], + item: Proto.NavigationTree, + ): boolean { let shouldInclude = TypeScriptDocumentSymbolProvider.shouldInclueEntry(item); + if (!shouldInclude && !item.childItems?.length) { + return false; + } const children = new Set(item.childItems || []); for (const span of item.spans) { @@ -83,7 +88,7 @@ class TypeScriptDocumentSymbolProvider implements vscode.DocumentSymbolProvider } if (shouldInclude) { - bucket.push(symbolInfo); + output.push(symbolInfo); } } @@ -98,7 +103,6 @@ class TypeScriptDocumentSymbolProvider implements vscode.DocumentSymbolProvider } } - export function register( selector: vscode.DocumentSelector, client: ITypeScriptServiceClient, diff --git a/extensions/typescript-language-features/src/features/referencesCodeLens.ts b/extensions/typescript-language-features/src/features/referencesCodeLens.ts index 5e92ce06e01..7aa228f8f8e 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 7ca35ed89cc..89689d57ae6 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 e785f1d4afb..06d284c3762 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 175804c6095..620ce762632 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -145,14 +145,7 @@ suite('window namespace tests', () => { }); }); - // TODO this randomly fails when running against web and visually - // what seems to happen is that the second editor opens and both - // left and right editor show a blinking cursor. Since active editor - // tracking relies on editor focus to function properly, this - // seems to be the root cause of the failure. - // https://github.com/microsoft/vscode/issues/90470 - test.skip('active editor not always correct... #49125', async function () { - + test('active editor not always correct... #49125', async function () { 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 a572fd95df8..c26c88671dd 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -214,104 +214,94 @@ suite('workspace-namespace', () => { }); }); - test('eol, change via onWillSave', function () { - if (vscode.env.uiKind === vscode.UIKind.Web) { - // TODO@Jo Test seems to fail when running in web due to - // onWillSaveTextDocument not getting called - return this.skip(); - } - + test('eol, change via onWillSave', async function () { let called = false; let sub = vscode.workspace.onWillSaveTextDocument(e => { called = true; e.waitUntil(Promise.resolve([vscode.TextEdit.setEndOfLine(vscode.EndOfLine.LF)])); }); - return createRandomFile('foo\r\nbar\r\nbar').then(file => { - return vscode.workspace.openTextDocument(file).then(doc => { - assert.equal(doc.eol, vscode.EndOfLine.CRLF); - const edit = new vscode.WorkspaceEdit(); - edit.set(file, [vscode.TextEdit.insert(new vscode.Position(0, 0), '-changes-')]); + const file = await createRandomFile('foo\r\nbar\r\nbar'); + const doc = await vscode.workspace.openTextDocument(file); + assert.equal(doc.eol, vscode.EndOfLine.CRLF); - return vscode.workspace.applyEdit(edit).then(success => { - assert.ok(success); - return doc.save(); + const edit = new vscode.WorkspaceEdit(); + edit.set(file, [vscode.TextEdit.insert(new vscode.Position(0, 0), '-changes-')]); + const successEdit = await vscode.workspace.applyEdit(edit); + assert.ok(successEdit); - }).then(success => { - assert.ok(success); - assert.ok(called); - assert.ok(!doc.isDirty); - assert.equal(doc.eol, vscode.EndOfLine.LF); - sub.dispose(); - }); - }); - }); + const successSave = await doc.save(); + assert.ok(successSave); + assert.ok(called); + assert.ok(!doc.isDirty); + assert.equal(doc.eol, vscode.EndOfLine.LF); + sub.dispose(); }); - test('events: onDidOpenTextDocument, onDidChangeTextDocument, onDidSaveTextDocument', () => { - return createRandomFile().then(file => { - let disposables: vscode.Disposable[] = []; + function assertEqualPath(a: string, b: string): void { + assert.ok(pathEquals(a, b), `${a} <-> ${b}`); + } - let onDidOpenTextDocument = false; - disposables.push(vscode.workspace.onDidOpenTextDocument(e => { - assert.ok(pathEquals(e.uri.fsPath, file.fsPath)); - onDidOpenTextDocument = true; - })); + test('events: onDidOpenTextDocument, onDidChangeTextDocument, onDidSaveTextDocument', async () => { + const file = await createRandomFile(); + let disposables: vscode.Disposable[] = []; - let onDidChangeTextDocument = false; - disposables.push(vscode.workspace.onDidChangeTextDocument(e => { - assert.ok(pathEquals(e.document.uri.fsPath, file.fsPath)); - onDidChangeTextDocument = true; - })); + await vscode.workspace.saveAll(); - let onDidSaveTextDocument = false; - disposables.push(vscode.workspace.onDidSaveTextDocument(e => { - assert.ok(pathEquals(e.uri.fsPath, file.fsPath)); - onDidSaveTextDocument = true; - })); + let pendingAsserts: Function[] = []; + let onDidOpenTextDocument = false; + disposables.push(vscode.workspace.onDidOpenTextDocument(e => { + pendingAsserts.push(() => assertEqualPath(e.uri.fsPath, file.fsPath)); + onDidOpenTextDocument = true; + })); - return vscode.workspace.openTextDocument(file).then(doc => { - return vscode.window.showTextDocument(doc).then((editor) => { - return editor.edit((builder) => { - builder.insert(new vscode.Position(0, 0), 'Hello World'); - }).then(_applied => { - return doc.save().then(_saved => { - assert.ok(onDidOpenTextDocument); - assert.ok(onDidChangeTextDocument); - assert.ok(onDidSaveTextDocument); + let onDidChangeTextDocument = false; + disposables.push(vscode.workspace.onDidChangeTextDocument(e => { + pendingAsserts.push(() => assertEqualPath(e.document.uri.fsPath, file.fsPath)); + onDidChangeTextDocument = true; + })); - disposeAll(disposables); + let onDidSaveTextDocument = false; + disposables.push(vscode.workspace.onDidSaveTextDocument(e => { + pendingAsserts.push(() => assertEqualPath(e.uri.fsPath, file.fsPath)); + onDidSaveTextDocument = true; + })); - return deleteFile(file); - }); - }); - }); - }); + const doc = await vscode.workspace.openTextDocument(file); + const editor = await vscode.window.showTextDocument(doc); + + await editor.edit((builder) => { + builder.insert(new vscode.Position(0, 0), 'Hello World'); }); + await doc.save(); + + assert.ok(onDidOpenTextDocument); + assert.ok(onDidChangeTextDocument); + assert.ok(onDidSaveTextDocument); + pendingAsserts.forEach(assert => assert()); + disposeAll(disposables); + return deleteFile(file); }); - test('events: onDidSaveTextDocument fires even for non dirty file when saved', () => { - return createRandomFile().then(file => { - let disposables: vscode.Disposable[] = []; + test('events: onDidSaveTextDocument fires even for non dirty file when saved', async () => { + const file = await createRandomFile(); + let disposables: vscode.Disposable[] = []; + let pendingAsserts: Function[] = []; - let onDidSaveTextDocument = false; - disposables.push(vscode.workspace.onDidSaveTextDocument(e => { - assert.ok(pathEquals(e.uri.fsPath, file.fsPath)); - onDidSaveTextDocument = true; - })); + let onDidSaveTextDocument = false; + disposables.push(vscode.workspace.onDidSaveTextDocument(e => { + pendingAsserts.push(() => assertEqualPath(e.uri.fsPath, file.fsPath)); + onDidSaveTextDocument = true; + })); - return vscode.workspace.openTextDocument(file).then(doc => { - return vscode.window.showTextDocument(doc).then(() => { - return vscode.commands.executeCommand('workbench.action.files.save').then(() => { - assert.ok(onDidSaveTextDocument); + const doc = await vscode.workspace.openTextDocument(file); + await vscode.window.showTextDocument(doc); + await vscode.commands.executeCommand('workbench.action.files.save'); - disposeAll(disposables); - - return deleteFile(file); - }); - }); - }); - }); + assert.ok(onDidSaveTextDocument); + pendingAsserts.forEach(fn => fn()); + disposeAll(disposables); + return deleteFile(file); }); test('openTextDocument, with selection', function () { diff --git a/package.json b/package.json index 3465c230a00..030a080b60b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.43.0", - "distro": "50679cbf1e8bf849cb5785226be6593d182153aa", + "distro": "956ab09303fe1ee416e492809a524357a6cc14a0", "author": { "name": "Microsoft Corporation" }, @@ -56,7 +56,7 @@ "vscode-ripgrep": "^1.5.8", "vscode-sqlite3": "4.0.9", "vscode-textmate": "4.4.0", - "xterm": "4.4.0", + "xterm": "4.5.0-beta.4", "xterm-addon-search": "0.5.0", "xterm-addon-unicode11": "0.1.1", "xterm-addon-web-links": "0.2.1", @@ -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", @@ -174,4 +174,4 @@ "windows-mutex": "0.3.0", "windows-process-tree": "0.2.4" } -} +} \ No newline at end of file diff --git a/remote/package.json b/remote/package.json index 14b25d4666b..7d3a3755dd3 100644 --- a/remote/package.json +++ b/remote/package.json @@ -20,7 +20,7 @@ "vscode-proxy-agent": "^0.5.2", "vscode-ripgrep": "^1.5.8", "vscode-textmate": "4.4.0", - "xterm": "4.4.0", + "xterm": "4.5.0-beta.4", "xterm-addon-search": "0.5.0", "xterm-addon-unicode11": "0.1.1", "xterm-addon-web-links": "0.2.1", diff --git a/remote/web/package.json b/remote/web/package.json index cdb1fd8db2e..ffcd7add280 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,7 +5,7 @@ "onigasm-umd": "2.2.5", "semver-umd": "^5.5.5", "vscode-textmate": "4.4.0", - "xterm": "4.4.0", + "xterm": "4.5.0-beta.4", "xterm-addon-search": "0.5.0", "xterm-addon-unicode11": "0.1.1", "xterm-addon-web-links": "0.2.1", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 0b1d4fb601b..c9f5c9ebb95 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -51,7 +51,7 @@ xterm-addon-webgl@0.5.0: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.5.0.tgz#c1031dc7599cce3509824643ab5f15361c928e3e" integrity sha512-hQrvabKCnwXFaEZ+YtoJM9Pm0CIBXL5KSwoU+RiGStU3KYTAcqYP2GsH3dWdvKX6kTWhWLS81dtDsGkfbOciuA== -xterm@4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0.tgz#5915d3c4c8800fadbcf555a0a603c672ab9df589" - integrity sha512-JGIpigWM3EBWvnS3rtBuefkiToIILSK1HYMXy4BCsUpO+O4UeeV+/U1AdAXgCB6qJrnPNb7yLgBsVCQUNMteig== +xterm@4.5.0-beta.4: + version "4.5.0-beta.4" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.5.0-beta.4.tgz#701f05553b643236d3fcd8bb7f14045bd4537c92" + integrity sha512-Yv1Bf60LTLBMaig1rv033hPz8hQGXZN6VYW2oe/409t2NbJXPg5xZgf47qyaWFV7a5k1BFiwjayJCWaL2nYBew== diff --git a/remote/yarn.lock b/remote/yarn.lock index 3fa3b053485..d576786f982 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -433,10 +433,10 @@ xterm-addon-webgl@0.5.0: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.5.0.tgz#c1031dc7599cce3509824643ab5f15361c928e3e" integrity sha512-hQrvabKCnwXFaEZ+YtoJM9Pm0CIBXL5KSwoU+RiGStU3KYTAcqYP2GsH3dWdvKX6kTWhWLS81dtDsGkfbOciuA== -xterm@4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0.tgz#5915d3c4c8800fadbcf555a0a603c672ab9df589" - integrity sha512-JGIpigWM3EBWvnS3rtBuefkiToIILSK1HYMXy4BCsUpO+O4UeeV+/U1AdAXgCB6qJrnPNb7yLgBsVCQUNMteig== +xterm@4.5.0-beta.4: + version "4.5.0-beta.4" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.5.0-beta.4.tgz#701f05553b643236d3fcd8bb7f14045bd4537c92" + integrity sha512-Yv1Bf60LTLBMaig1rv033hPz8hQGXZN6VYW2oe/409t2NbJXPg5xZgf47qyaWFV7a5k1BFiwjayJCWaL2nYBew== yauzl@^2.9.2: version "2.10.0" diff --git a/scripts/code-web.js b/scripts/code-web.js index 26fb980e3d5..4556e7d7e6d 100755 --- a/scripts/code-web.js +++ b/scripts/code-web.js @@ -66,11 +66,11 @@ const server = http.createServer((req, res) => { // manifest res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ - "name": "Code Web - OSS", - "short_name": "Code Web - OSS", - "start_url": "/", - "lang": "en-US", - "display": "standalone" + 'name': 'Code Web - OSS', + 'short_name': 'Code Web - OSS', + 'start_url': '/', + 'lang': 'en-US', + 'display': 'standalone' })); } if (/^\/static\//.test(pathname)) { diff --git a/scripts/code.sh b/scripts/code.sh index 4ba1a00b9f7..0afe96bfcb6 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/scripts/test-integration.bat b/scripts/test-integration.bat index b8d673f70d4..cb7271a9152 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -15,14 +15,16 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( echo Running integration tests out of sources. ) else ( :: Run from a built: need to compile all test extensions - call yarn gulp compile-extension:vscode-api-tests - call yarn gulp compile-extension:vscode-colorize-tests - call yarn gulp compile-extension:markdown-language-features - call yarn gulp compile-extension:emmet - call yarn gulp compile-extension:css-language-features-server - call yarn gulp compile-extension:html-language-features-server - call yarn gulp compile-extension:json-language-features-server - call yarn gulp compile-extension:git + :: because we run extension tests from their source folders + :: and the build bundles extensions into .build webpacked + call yarn gulp compile-extension:vscode-api-tests^ + compile-extension:vscode-colorize-tests^ + compile-extension:markdown-language-features^ + compile-extension:emmet^ + compile-extension:css-language-features-server^ + compile-extension:html-language-features-server^ + compile-extension:json-language-features-server^ + compile-extension:git :: Configuration for more verbose output set VSCODE_CLI=1 diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 9aff685b7c6..6659549f001 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -21,14 +21,16 @@ then echo "Running integration tests out of sources." else # Run from a built: need to compile all test extensions - yarn gulp compile-extension:vscode-api-tests - yarn gulp compile-extension:vscode-colorize-tests - yarn gulp compile-extension:markdown-language-features - yarn gulp compile-extension:emmet - yarn gulp compile-extension:css-language-features-server - yarn gulp compile-extension:html-language-features-server - yarn gulp compile-extension:json-language-features-server - yarn gulp compile-extension:git + # because we run extension tests from their source folders + # and the build bundles extensions into .build webpacked + yarn gulp compile-extension:vscode-api-tests \ + compile-extension:vscode-colorize-tests \ + compile-extension:markdown-language-features \ + compile-extension:emmet \ + compile-extension:css-language-features-server \ + compile-extension:html-language-features-server \ + compile-extension:json-language-features-server \ + compile-extension:git # Configuration for more verbose output export VSCODE_CLI=1 diff --git a/src/main.js b/src/main.js index 7242b0cb156..6a6cb060acc 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 c017682c7f6..2fa65d71923 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?be537a78617db0869caa4b4cc683a24a") format("truetype"); + src: url("./codicon.ttf?d0510f6ecacbb2788db2b3162273a3d8") format("truetype"); } .codicon[class*='codicon-'] { @@ -413,4 +413,5 @@ .codicon-feedback:before { content: "\eb96" } .codicon-group-by-ref-type:before { content: "\eb97" } .codicon-ungroup-by-ref-type:before { content: "\eb98" } -.codicon-debug-alt:before { content: "\f101" } +.codicon-debug-alt-2:before { content: "\f101" } +.codicon-debug-alt:before { content: "\f102" } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf index 90ace76ff74..f712f5cd053 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/async.ts b/src/vs/base/common/async.ts index 81c29e6731b..ceb38bd5b18 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -779,3 +779,107 @@ export async function retry(task: ITask>, delay: number, retries: throw lastError; } + +//#region Task Sequentializer + +interface IPendingTask { + taskId: number; + cancel: () => void; + promise: Promise; +} + +interface ISequentialTask { + promise: Promise; + promiseResolve: () => void; + promiseReject: (error: Error) => void; + run: () => Promise; +} + +export interface ITaskSequentializerWithPendingTask { + readonly pending: Promise; +} + +export class TaskSequentializer { + private _pending?: IPendingTask; + private _next?: ISequentialTask; + + hasPending(taskId?: number): this is ITaskSequentializerWithPendingTask { + if (!this._pending) { + return false; + } + + if (typeof taskId === 'number') { + return this._pending.taskId === taskId; + } + + return !!this._pending; + } + + get pending(): Promise | undefined { + return this._pending ? this._pending.promise : undefined; + } + + cancelPending(): void { + this._pending?.cancel(); + } + + setPending(taskId: number, promise: Promise, onCancel?: () => void, ): Promise { + this._pending = { taskId: taskId, cancel: () => onCancel?.(), promise }; + + promise.then(() => this.donePending(taskId), () => this.donePending(taskId)); + + return promise; + } + + private donePending(taskId: number): void { + if (this._pending && taskId === this._pending.taskId) { + + // only set pending to done if the promise finished that is associated with that taskId + this._pending = undefined; + + // schedule the next task now that we are free if we have any + this.triggerNext(); + } + } + + private triggerNext(): void { + if (this._next) { + const next = this._next; + this._next = undefined; + + // Run next task and complete on the associated promise + next.run().then(next.promiseResolve, next.promiseReject); + } + } + + setNext(run: () => Promise): Promise { + + // this is our first next task, so we create associated promise with it + // so that we can return a promise that completes when the task has + // completed. + if (!this._next) { + let promiseResolve: () => void; + let promiseReject: (error: Error) => void; + const promise = new Promise((resolve, reject) => { + promiseResolve = resolve; + promiseReject = reject; + }); + + this._next = { + run, + promise, + promiseResolve: promiseResolve!, + promiseReject: promiseReject! + }; + } + + // we have a previous next task, just overwrite it + else { + this._next.run = run; + } + + return this._next.promise; + } +} + +//#endregion diff --git a/src/vs/base/common/date.ts b/src/vs/base/common/date.ts index a6da9d5ae33..acfc19d6edb 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/common/glob.ts b/src/vs/base/common/glob.ts index 78fc5e97ce1..6da220436f6 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -251,14 +251,14 @@ export interface IGlobOptions { } interface ParsedStringPattern { - (path: string, basename: string): string | null | Promise /* the matching pattern */; + (path: string, basename?: string): string | null | Promise /* the matching pattern */; basenames?: string[]; patterns?: string[]; allBasenames?: string[]; allPaths?: string[]; } interface ParsedExpressionPattern { - (path: string, basename: string, name?: string, hasSibling?: (name: string) => boolean | Promise): string | null | Promise /* the matching pattern */; + (path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise): string | null | Promise /* the matching pattern */; requiresSiblings?: boolean; allBasenames?: string[]; allPaths?: string[]; @@ -374,7 +374,7 @@ function trivia3(pattern: string, options: IGlobOptions): ParsedStringPattern { if (n === 1) { return parsedPatterns[0]; } - const parsedPattern: ParsedStringPattern = function (path: string, basename: string) { + const parsedPattern: ParsedStringPattern = function (path: string, basename?: string) { for (let i = 0, n = parsedPatterns.length; i < n; i++) { if ((parsedPatterns[i])(path, basename)) { return pattern; @@ -409,7 +409,7 @@ function trivia4and5(path: string, pattern: string, matchPathEnds: boolean): Par function toRegExp(pattern: string): ParsedStringPattern { try { const regExp = new RegExp(`^${parseRegExp(pattern)}$`); - return function (path: string, basename: string) { + return function (path: string) { regExp.lastIndex = 0; // reset RegExp to its initial state to reuse it! return typeof path === 'string' && regExp.test(path) ? pattern : null; }; @@ -457,7 +457,7 @@ export function parse(arg1: string | IExpression | IRelativePattern, options: IG if (parsedPattern === NULL) { return FALSE; } - const resultPattern: ParsedPattern & { allBasenames?: string[]; allPaths?: string[]; } = function (path: string, basename: string) { + const resultPattern: ParsedPattern & { allBasenames?: string[]; allPaths?: string[]; } = function (path: string, basename?: string) { return !!parsedPattern(path, basename); }; if (parsedPattern.allBasenames) { @@ -540,7 +540,7 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse return parsedPatterns[0]; } - const resultExpression: ParsedStringPattern = function (path: string, basename: string) { + const resultExpression: ParsedStringPattern = function (path: string, basename?: string) { for (let i = 0, n = parsedPatterns.length; i < n; i++) { // Pattern matches path const result = (parsedPatterns[i])(path, basename); @@ -565,7 +565,7 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse return resultExpression; } - const resultExpression: ParsedStringPattern = function (path: string, basename: string, hasSibling?: (name: string) => boolean | Promise) { + const resultExpression: ParsedStringPattern = function (path: string, basename?: string, hasSibling?: (name: string) => boolean | Promise) { let name: string | undefined = undefined; for (let i = 0, n = parsedPatterns.length; i < n; i++) { @@ -620,12 +620,12 @@ function parseExpressionPattern(pattern: string, value: boolean | SiblingClause, if (value) { const when = (value).when; if (typeof when === 'string') { - const result: ParsedExpressionPattern = (path: string, basename: string, name: string, hasSibling: (name: string) => boolean | Promise) => { + const result: ParsedExpressionPattern = (path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise) => { if (!hasSibling || !parsedPattern(path, basename)) { return null; } - const clausePattern = when.replace('$(basename)', name); + const clausePattern = when.replace('$(basename)', name!); const matched = hasSibling(clausePattern); return isThenable(matched) ? matched.then(m => m ? pattern : null) : 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 3d69948aa42..ee268d9fdcd 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/common/async.test.ts b/src/vs/base/test/common/async.test.ts index 586c0fa8a7f..dda98ab6e1c 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -557,4 +557,93 @@ suite('Async', () => { assert.equal(error, error); } }); + + test('TaskSequentializer - pending basics', async function () { + const sequentializer = new async.TaskSequentializer(); + + assert.ok(!sequentializer.hasPending()); + assert.ok(!sequentializer.hasPending(2323)); + assert.ok(!sequentializer.pending); + + // pending removes itself after done + await sequentializer.setPending(1, Promise.resolve()); + assert.ok(!sequentializer.hasPending()); + assert.ok(!sequentializer.hasPending(1)); + assert.ok(!sequentializer.pending); + + // pending removes itself after done (use async.timeout) + sequentializer.setPending(2, async.timeout(1)); + assert.ok(sequentializer.hasPending()); + assert.ok(sequentializer.hasPending(2)); + assert.ok(!sequentializer.hasPending(1)); + assert.ok(sequentializer.pending); + + await async.timeout(2); + assert.ok(!sequentializer.hasPending()); + assert.ok(!sequentializer.hasPending(2)); + assert.ok(!sequentializer.pending); + }); + + test('TaskSequentializer - pending and next (finishes instantly)', async function () { + const sequentializer = new async.TaskSequentializer(); + + let pendingDone = false; + sequentializer.setPending(1, async.timeout(1).then(() => { pendingDone = true; return; })); + + // next finishes instantly + let nextDone = false; + const res = sequentializer.setNext(() => Promise.resolve(null).then(() => { nextDone = true; return; })); + + await res; + assert.ok(pendingDone); + assert.ok(nextDone); + }); + + test('TaskSequentializer - pending and next (finishes after timeout)', async function () { + const sequentializer = new async.TaskSequentializer(); + + let pendingDone = false; + sequentializer.setPending(1, async.timeout(1).then(() => { pendingDone = true; return; })); + + // next finishes after async.timeout + let nextDone = false; + const res = sequentializer.setNext(() => async.timeout(1).then(() => { nextDone = true; return; })); + + await res; + assert.ok(pendingDone); + assert.ok(nextDone); + }); + + test('TaskSequentializer - pending and multiple next (last one wins)', async function () { + const sequentializer = new async.TaskSequentializer(); + + let pendingDone = false; + sequentializer.setPending(1, async.timeout(1).then(() => { pendingDone = true; return; })); + + // next finishes after async.timeout + let firstDone = false; + let firstRes = sequentializer.setNext(() => async.timeout(2).then(() => { firstDone = true; return; })); + + let secondDone = false; + let secondRes = sequentializer.setNext(() => async.timeout(3).then(() => { secondDone = true; return; })); + + let thirdDone = false; + let thirdRes = sequentializer.setNext(() => async.timeout(4).then(() => { thirdDone = true; return; })); + + await Promise.all([firstRes, secondRes, thirdRes]); + assert.ok(pendingDone); + assert.ok(!firstDone); + assert.ok(!secondDone); + assert.ok(thirdDone); + }); + + test('TaskSequentializer - cancel pending', async function () { + const sequentializer = new async.TaskSequentializer(); + + let pendingCancelled = false; + sequentializer.setPending(1, async.timeout(1), () => pendingCancelled = true); + sequentializer.cancelPending(); + + assert.ok(pendingCancelled); + }); }); 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 2c765555ffe..8e1cb887485 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 d17a34fbf0c..45f6f17ce06 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/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 9e2970756db..b0992f3baa0 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -17,7 +17,7 @@ import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtension import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { IRequestService } from 'vs/platform/request/common/request'; import { RequestService } from 'vs/platform/request/browser/requestService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 1c098b9e8f0..7871c40dc59 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -26,7 +26,7 @@ import { IStateService } from 'vs/platform/state/node/state'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { EnvironmentService, xdgRuntimeDir } from 'vs/platform/environment/node/environmentService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { IRequestService } from 'vs/platform/request/common/request'; import { RequestMainService } from 'vs/platform/request/electron-main/requestMainService'; import * as fs from 'fs'; diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index f2e8279010b..9c82546995f 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -24,7 +24,7 @@ import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProper import { IRequestService } from 'vs/platform/request/common/request'; import { RequestService } from 'vs/platform/request/node/requestService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; import { mkdirp, writeFile } from 'vs/base/node/pfs'; import { getBaseLabel } from 'vs/base/common/labels'; diff --git a/src/vs/editor/browser/controller/coreCommands.ts b/src/vs/editor/browser/controller/coreCommands.ts index ab975aa915d..13ec59bc49f 100644 --- a/src/vs/editor/browser/controller/coreCommands.ts +++ b/src/vs/editor/browser/controller/coreCommands.ts @@ -1614,7 +1614,7 @@ export namespace CoreEditingCommands { constructor() { super({ id: 'deleteLeft', - precondition: EditorContextKeys.writable, + precondition: undefined, kbOpts: { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, @@ -1639,7 +1639,7 @@ export namespace CoreEditingCommands { constructor() { super({ id: 'deleteRight', - precondition: EditorContextKeys.writable, + precondition: undefined, kbOpts: { weight: CORE_WEIGHT, kbExpr: EditorContextKeys.textInputFocus, diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index a33a4ec776f..4dfb0035522 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -103,7 +103,15 @@ export class OpenerService implements IOpenerService { // Default external opener is going through window.open() this._externalOpener = { openExternal: href => { - dom.windowOpenNoOpener(href); + // ensure to open HTTP/HTTPS links into new windows + // to not trigger a navigation. Any other link is + // safe to be set as HREF to prevent a blank window + // from opening. + if (matchesScheme(href, Schemas.http) || matchesScheme(href, Schemas.https)) { + dom.windowOpenNoOpener(href); + } else { + window.location.href = href; + } return Promise.resolve(true); } }; diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 600f303335e..81611253b01 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; @@ -1158,20 +1724,17 @@ registerThemingParticipant((theme, collector) => { if (minimapBackgroundValue) { collector.addRule(`.monaco-editor .minimap > canvas { opacity: ${minimapBackgroundValue.rgba.a}; will-change: opacity; }`); } - const sliderBackground = theme.getColor(scrollbarSliderBackground); + const sliderBackground = theme.getColor(minimapSliderBackground); if (sliderBackground) { - const halfSliderBackground = sliderBackground.transparent(0.5); - collector.addRule(`.monaco-editor .minimap-slider, .monaco-editor .minimap-slider .minimap-slider-horizontal { background: ${halfSliderBackground}; }`); + collector.addRule(`.monaco-editor .minimap-slider .minimap-slider-horizontal { background: ${sliderBackground}; }`); } - const sliderHoverBackground = theme.getColor(scrollbarSliderHoverBackground); + const sliderHoverBackground = theme.getColor(minimapSliderHoverBackground); if (sliderHoverBackground) { - const halfSliderHoverBackground = sliderHoverBackground.transparent(0.5); - collector.addRule(`.monaco-editor .minimap-slider:hover, .monaco-editor .minimap-slider:hover .minimap-slider-horizontal { background: ${halfSliderHoverBackground}; }`); + collector.addRule(`.monaco-editor .minimap-slider:hover .minimap-slider-horizontal { background: ${sliderHoverBackground}; }`); } - const sliderActiveBackground = theme.getColor(scrollbarSliderActiveBackground); + const sliderActiveBackground = theme.getColor(minimapSliderActiveBackground); if (sliderActiveBackground) { - const halfSliderActiveBackground = sliderActiveBackground.transparent(0.5); - collector.addRule(`.monaco-editor .minimap-slider.active, .monaco-editor .minimap-slider.active .minimap-slider-horizontal { background: ${halfSliderActiveBackground}; }`); + collector.addRule(`.monaco-editor .minimap-slider.active .minimap-slider-horizontal { background: ${sliderActiveBackground}; }`); } const shadow = theme.getColor(scrollbarShadow); if (shadow) { diff --git a/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts b/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts index 6feeecc03ac..c30b720c207 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 0cbac3c5035..bd1aa10f8a8 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 ed8c20c1674..7f488a873dc 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 50d6dd6c3e0..81949c64047 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 2c1fcc8f248..8aa9b5a53d4 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 11fc0e4165f..a09a1abf0fe 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 e396aa054a2..c662f820c9c 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 7d925b4bc2c..0340900a5c4 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/modes/languageConfigurationRegistry.ts b/src/vs/editor/common/modes/languageConfigurationRegistry.ts index eacb7ed17b9..daadeebb57e 100644 --- a/src/vs/editor/common/modes/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/modes/languageConfigurationRegistry.ts @@ -274,6 +274,16 @@ export class LanguageConfigurationRegistryImpl { return ensureValidWordDefinition(value.wordDefinition || null); } + public getWordDefinitions(): [LanguageId, RegExp][] { + let result: [LanguageId, RegExp][] = []; + this._entries.forEach((value, language) => { + if (value) { + result.push([language, value.wordDefinition]); + } + }); + return result; + } + public getFoldingRules(languageId: LanguageId): FoldingRules { let value = this._getRichEditSupport(languageId); if (!value) { diff --git a/src/vs/editor/common/view/viewEvents.ts b/src/vs/editor/common/view/viewEvents.ts index 1d9290b5e76..0f2720d8514 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 ed3b3382535..171a4839949 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 96c7b3a0d17..440c78fef72 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/contrib/parameterHints/parameterHints.css b/src/vs/editor/contrib/parameterHints/parameterHints.css index 89bf6462e7f..03c4e2640ee 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/parameterHints.css @@ -33,6 +33,7 @@ .monaco-editor .parameter-hints-widget .monaco-scrollable-element, .monaco-editor .parameter-hints-widget .body { display: flex; + flex: 1; flex-direction: column; min-height: 100%; } diff --git a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts index 2b19d53b201..249380f044a 100644 --- a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts @@ -180,9 +180,11 @@ suite('SmartSelect', () => { // -- bracket selections async function assertRanges(provider: SelectionRangeProvider, value: string, ...expected: IRange[]): Promise { + let index = value.indexOf('|'); + value = value.replace('|', ''); let model = modelService.createModel(value, new StaticLanguageSelector(mode.getLanguageIdentifier()), URI.parse('fake:lang')); - let pos = model.getPositionAt(value.indexOf('|')); + let pos = model.getPositionAt(index); let all = await provider.provideSelectionRanges(model, [pos], CancellationToken.None); let ranges = all![0]; @@ -197,18 +199,18 @@ suite('SmartSelect', () => { test('bracket selection', async () => { await assertRanges(new BracketSelectionRangeProvider(), '(|)', - new Range(1, 2, 1, 3), new Range(1, 1, 1, 4) + new Range(1, 2, 1, 2), new Range(1, 1, 1, 3) ); await assertRanges(new BracketSelectionRangeProvider(), '[[[](|)]]', - new Range(1, 6, 1, 7), new Range(1, 5, 1, 8), // () - new Range(1, 3, 1, 8), new Range(1, 2, 1, 9), // [[]()] - new Range(1, 2, 1, 9), new Range(1, 1, 1, 10), // [[[]()]] + new Range(1, 6, 1, 6), new Range(1, 5, 1, 7), // () + new Range(1, 3, 1, 7), new Range(1, 2, 1, 8), // [[]()] + new Range(1, 2, 1, 8), new Range(1, 1, 1, 9), // [[[]()]] ); await assertRanges(new BracketSelectionRangeProvider(), '[a[](|)a]', - new Range(1, 6, 1, 7), new Range(1, 5, 1, 8), - new Range(1, 2, 1, 9), new Range(1, 1, 1, 10), + new Range(1, 6, 1, 6), new Range(1, 5, 1, 7), + new Range(1, 2, 1, 8), new Range(1, 1, 1, 9), ); // no bracket @@ -219,23 +221,23 @@ suite('SmartSelect', () => { await assertRanges(new BracketSelectionRangeProvider(), '|[[[]()]]'); // edge - await assertRanges(new BracketSelectionRangeProvider(), '[|[[]()]]', new Range(1, 2, 1, 9), new Range(1, 1, 1, 10)); - await assertRanges(new BracketSelectionRangeProvider(), '[[[]()]|]', new Range(1, 2, 1, 9), new Range(1, 1, 1, 10)); + await assertRanges(new BracketSelectionRangeProvider(), '[|[[]()]]', new Range(1, 2, 1, 8), new Range(1, 1, 1, 9)); + await assertRanges(new BracketSelectionRangeProvider(), '[[[]()]|]', new Range(1, 2, 1, 8), new Range(1, 1, 1, 9)); - await assertRanges(new BracketSelectionRangeProvider(), 'aaa(aaa)bbb(b|b)ccc(ccc)', new Range(1, 13, 1, 16), new Range(1, 12, 1, 17)); - await assertRanges(new BracketSelectionRangeProvider(), '(aaa(aaa)bbb(b|b)ccc(ccc))', new Range(1, 14, 1, 17), new Range(1, 13, 1, 18), new Range(1, 2, 1, 26), new Range(1, 1, 1, 27)); + await assertRanges(new BracketSelectionRangeProvider(), 'aaa(aaa)bbb(b|b)ccc(ccc)', new Range(1, 13, 1, 15), new Range(1, 12, 1, 16)); + await assertRanges(new BracketSelectionRangeProvider(), '(aaa(aaa)bbb(b|b)ccc(ccc))', new Range(1, 14, 1, 16), new Range(1, 13, 1, 17), new Range(1, 2, 1, 25), new Range(1, 1, 1, 26)); }); test('bracket with leading/trailing', async () => { await assertRanges(new BracketSelectionRangeProvider(), 'for(a of b){\n foo(|);\n}', - new Range(2, 7, 2, 8), new Range(2, 6, 2, 9), + new Range(2, 7, 2, 7), new Range(2, 6, 2, 8), new Range(1, 13, 3, 1), new Range(1, 12, 3, 2), new Range(1, 1, 3, 2), new Range(1, 1, 3, 2), ); await assertRanges(new BracketSelectionRangeProvider(), 'for(a of b)\n{\n foo(|);\n}', - new Range(3, 7, 3, 8), new Range(3, 6, 3, 9), + new Range(3, 7, 3, 7), new Range(3, 6, 3, 8), new Range(2, 2, 4, 1), new Range(2, 1, 4, 2), new Range(1, 1, 4, 2), new Range(1, 1, 4, 2), ); @@ -244,60 +246,60 @@ suite('SmartSelect', () => { test('in-word ranges', async () => { await assertRanges(new WordSelectionRangeProvider(), 'f|ooBar', - new Range(1, 1, 1, 5), // foo - new Range(1, 1, 1, 8), // fooBar - new Range(1, 1, 1, 8), // doc + new Range(1, 1, 1, 4), // foo + new Range(1, 1, 1, 7), // fooBar + new Range(1, 1, 1, 7), // doc ); await assertRanges(new WordSelectionRangeProvider(), 'f|oo_Ba', - new Range(1, 1, 1, 5), - new Range(1, 1, 1, 8), - new Range(1, 1, 1, 8), + new Range(1, 1, 1, 4), + new Range(1, 1, 1, 7), + new Range(1, 1, 1, 7), ); await assertRanges(new WordSelectionRangeProvider(), 'f|oo-Ba', - new Range(1, 1, 1, 5), - new Range(1, 1, 1, 8), - new Range(1, 1, 1, 8), + new Range(1, 1, 1, 4), + new Range(1, 1, 1, 7), + new Range(1, 1, 1, 7), ); }); test('Default selection should select current word/hump first in camelCase #67493', async function () { await assertRanges(new WordSelectionRangeProvider(), 'Abs|tractSmartSelect', - new Range(1, 1, 1, 10), - new Range(1, 1, 1, 21), - new Range(1, 1, 1, 21), + new Range(1, 1, 1, 9), + new Range(1, 1, 1, 20), + new Range(1, 1, 1, 20), ); await assertRanges(new WordSelectionRangeProvider(), 'AbstractSma|rtSelect', - new Range(1, 9, 1, 15), - new Range(1, 1, 1, 21), - new Range(1, 1, 1, 21), + new Range(1, 9, 1, 14), + new Range(1, 1, 1, 20), + new Range(1, 1, 1, 20), ); await assertRanges(new WordSelectionRangeProvider(), 'Abstrac-Sma|rt-elect', - new Range(1, 9, 1, 15), - new Range(1, 1, 1, 21), - new Range(1, 1, 1, 21), + new Range(1, 9, 1, 14), + new Range(1, 1, 1, 20), + new Range(1, 1, 1, 20), ); await assertRanges(new WordSelectionRangeProvider(), 'Abstrac_Sma|rt_elect', - new Range(1, 9, 1, 15), - new Range(1, 1, 1, 21), - new Range(1, 1, 1, 21), + new Range(1, 9, 1, 14), + new Range(1, 1, 1, 20), + new Range(1, 1, 1, 20), ); await assertRanges(new WordSelectionRangeProvider(), 'Abstrac_Sma|rt-elect', - new Range(1, 9, 1, 15), - new Range(1, 1, 1, 21), - new Range(1, 1, 1, 21), + new Range(1, 9, 1, 14), + new Range(1, 1, 1, 20), + new Range(1, 1, 1, 20), ); await assertRanges(new WordSelectionRangeProvider(), 'Abstrac_Sma|rtSelect', - new Range(1, 9, 1, 15), - new Range(1, 1, 1, 21), - new Range(1, 1, 1, 21), + new Range(1, 9, 1, 14), + new Range(1, 1, 1, 20), + new Range(1, 1, 1, 20), ); }); @@ -321,4 +323,49 @@ suite('SmartSelect', () => { reg.dispose(); }); + + test('Expand selection in words with underscores is inconsistent #90589', async function () { + + await assertRanges(new WordSelectionRangeProvider(), 'Hel|lo_World', + new Range(1, 1, 1, 6), + new Range(1, 1, 1, 12), + new Range(1, 1, 1, 12), + ); + + await assertRanges(new WordSelectionRangeProvider(), 'Hello_Wo|rld', + new Range(1, 7, 1, 12), + new Range(1, 1, 1, 12), + new Range(1, 1, 1, 12), + ); + + await assertRanges(new WordSelectionRangeProvider(), 'Hello|_World', + new Range(1, 1, 1, 6), + new Range(1, 1, 1, 12), + new Range(1, 1, 1, 12), + ); + + await assertRanges(new WordSelectionRangeProvider(), 'Hello_|World', + new Range(1, 7, 1, 12), + new Range(1, 1, 1, 12), + new Range(1, 1, 1, 12), + ); + + await assertRanges(new WordSelectionRangeProvider(), 'Hello|-World', + new Range(1, 1, 1, 6), + new Range(1, 1, 1, 12), + new Range(1, 1, 1, 12), + ); + + await assertRanges(new WordSelectionRangeProvider(), 'Hello-|World', + new Range(1, 7, 1, 12), + new Range(1, 1, 1, 12), + new Range(1, 1, 1, 12), + ); + + await assertRanges(new WordSelectionRangeProvider(), 'Hello|World', + new Range(1, 6, 1, 11), + new Range(1, 1, 1, 11), + new Range(1, 1, 1, 11), + ); + }); }); diff --git a/src/vs/editor/contrib/smartSelect/wordSelections.ts b/src/vs/editor/contrib/smartSelect/wordSelections.ts index 7402202af54..280663f59a8 100644 --- a/src/vs/editor/contrib/smartSelect/wordSelections.ts +++ b/src/vs/editor/contrib/smartSelect/wordSelections.ts @@ -40,7 +40,7 @@ export class WordSelectionRangeProvider implements SelectionRangeProvider { // LEFT anchor (start) for (; start >= 0; start--) { let ch = word.charCodeAt(start); - if (ch === CharCode.Underline || ch === CharCode.Dash) { + if ((start !== offset) && (ch === CharCode.Underline || ch === CharCode.Dash)) { // foo-bar OR foo_bar break; } else if (isLowerAsciiLetter(ch) && isUpperAsciiLetter(lastCh)) { diff --git a/src/vs/editor/contrib/suggest/media/suggest.css b/src/vs/editor/contrib/suggest/media/suggest.css index 2176471c248..821cb7d2623 100644 --- a/src/vs/editor/contrib/suggest/media/suggest.css +++ b/src/vs/editor/contrib/suggest/media/suggest.css @@ -137,8 +137,6 @@ border-bottom-style: solid; padding: 0 8px 0 4px; - - box-shadow: 0 -.5px 3px #ddd; } .monaco-editor .suggest-widget.list-right.docs-side > .suggest-status-bar { @@ -177,6 +175,11 @@ opacity: 0.7; } +.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .left > .signature-label { + overflow: auto; + text-overflow: ellipsis; +} + .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .left > .qualifier-label { margin-left: 4px; opacity: 0.4; @@ -213,8 +216,8 @@ /** Details: if using CompletionItemLabel#details, always show **/ -.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right.always-show-details > .details-label, -.monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row.focused > .contents > .main > .right.always-show-details > .details-label { +.monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) > .contents > .main > .right > .details-label, +.monaco-editor .suggest-widget.docs-side .monaco-list .monaco-list-row.focused:not(.string-label) > .contents > .main > .right > .details-label { display: inline; } @@ -228,6 +231,12 @@ overflow: hidden; } .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .left > .monaco-icon-label { + flex-shrink: 0; +} +.monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) > .contents > .main > .left > .monaco-icon-label { + max-width: 80%; +} +.monaco-editor .suggest-widget .monaco-list .monaco-list-row.string-label > .contents > .main > .left > .monaco-icon-label { flex-shrink: 1; } .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right { @@ -253,11 +262,11 @@ } /** Do NOT display ReadMore when using plain CompletionItemLabel (details/documentation might not be resolved) **/ -.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right:not(.always-show-details) > .readMore { +.monaco-editor .suggest-widget .monaco-list .monaco-list-row.string-label > .contents > .main > .right > .readMore { display: none; } /** Focused item can show ReadMore, but can't when docs is side/below **/ -.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused > .contents > .main > .right:not(.always-show-details) > .readMore { +.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused.string-label > .contents > .main > .right > .readMore { display: inline-block; } diff --git a/src/vs/editor/contrib/suggest/media/suggestStatusBar.css b/src/vs/editor/contrib/suggest/media/suggestStatusBar.css index 1b0014ec452..f645f3c0240 100644 --- a/src/vs/editor/contrib/suggest/media/suggestStatusBar.css +++ b/src/vs/editor/contrib/suggest/media/suggestStatusBar.css @@ -26,7 +26,7 @@ } .monaco-editor .suggest-widget.with-status-bar .monaco-list .monaco-list-row > .contents > .main > .right > .readMore, -.monaco-editor .suggest-widget.with-status-bar .monaco-list .monaco-list-row.focused > .contents > .main > .right:not(.always-show-details) > .readMore { +.monaco-editor .suggest-widget.with-status-bar .monaco-list .monaco-list-row.focused.string-label > .contents > .main > .right > .readMore { display: none; } diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 47362e74e7c..5baa237e29a 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -244,12 +244,12 @@ class ItemRenderer implements IListRenderer= 0 ? wordLines[idx] : wordLines[Math.max(0, ~idx - 1)]; let blockDistance = ranges.length; - for (const range of ranges[0]) { + for (const range of ranges) { if (!Range.containsRange(range.range, bestWordRange)) { break; } diff --git a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts index 0fa9fe4caa7..15ea81e6a44 100644 --- a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts @@ -11,6 +11,8 @@ import { Selection } from 'vs/editor/common/core/selection'; import { deserializePipePositions, serializePipePositions, testRepeatedActionAndExtractPositions } from 'vs/editor/contrib/wordOperations/test/wordTestUtils'; import { CursorWordEndLeft, CursorWordEndLeftSelect, CursorWordEndRight, CursorWordEndRightSelect, CursorWordLeft, CursorWordLeftSelect, CursorWordRight, CursorWordRightSelect, CursorWordStartLeft, CursorWordStartLeftSelect, CursorWordStartRight, CursorWordStartRightSelect, DeleteWordEndLeft, DeleteWordEndRight, DeleteWordLeft, DeleteWordRight, DeleteWordStartLeft, DeleteWordStartRight, CursorWordAccessibilityLeft, CursorWordAccessibilityLeftSelect, CursorWordAccessibilityRight, CursorWordAccessibilityRightSelect } from 'vs/editor/contrib/wordOperations/wordOperations'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { Handler } from 'vs/editor/common/editorCommon'; +import { Cursor } from 'vs/editor/common/controller/cursor'; suite('WordOperations', () => { @@ -193,6 +195,32 @@ suite('WordOperations', () => { assert.deepEqual(actual, EXPECTED); }); + test('issue #51275 - cursorWordStartLeft does not push undo/redo stack element', () => { + function cursorCommand(cursor: Cursor, command: string, extraData?: any, overwriteSource?: string) { + cursor.trigger(overwriteSource || 'tests', command, extraData); + } + + function type(cursor: Cursor, text: string) { + for (let i = 0; i < text.length; i++) { + cursorCommand(cursor, Handler.Type, { text: text.charAt(i) }, 'keyboard'); + } + } + + withTestCodeEditor('', {}, (editor, cursor) => { + type(cursor, 'foo bar baz'); + assert.equal(editor.getValue(), 'foo bar baz'); + + cursorWordStartLeft(editor); + cursorWordStartLeft(editor); + type(cursor, 'q'); + + assert.equal(editor.getValue(), 'foo qbar baz'); + + cursorCommand(cursor, Handler.Undo, {}); + assert.equal(editor.getValue(), 'foo bar baz'); + }); + }); + test('cursorWordEndLeft', () => { const EXPECTED = ['| /*| Just| some| more| text| a|+=| 3| +|5|-|3| +| 7| */| '].join('\n'); const [text,] = deserializePipePositions(EXPECTED); diff --git a/src/vs/editor/contrib/wordOperations/wordOperations.ts b/src/vs/editor/contrib/wordOperations/wordOperations.ts index 57d9174719d..ae5429ed0cc 100644 --- a/src/vs/editor/contrib/wordOperations/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/wordOperations.ts @@ -52,6 +52,7 @@ export abstract class MoveWordCommand extends EditorCommand { return this._moveTo(sel, outPosition, this._inSelectionMode); }); + model.pushStackElement(); editor._getCursors().setStates('moveWordCommand', CursorChangeReason.NotSet, result.map(r => CursorState.fromModelSelection(r))); if (result.length === 1) { const pos = new Position(result[0].positionLineNumber, result[0].positionColumn); diff --git a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts index 3c32536b239..08d629e4260 100644 --- a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts +++ b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts @@ -6,6 +6,7 @@ import 'vs/css!./inspectTokens'; import { CharCode } from 'vs/base/common/charCode'; import { Color } from 'vs/base/common/color'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; import { escape } from 'vs/base/common/strings'; import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; @@ -48,6 +49,7 @@ class InspectTokensController extends Disposable implements IEditorContribution this._register(this._editor.onDidChangeModel((e) => this.stop())); this._register(this._editor.onDidChangeModelLanguage((e) => this.stop())); this._register(TokenizationRegistry.onDidChange((e) => this.stop())); + this._register(this._editor.onKeyUp((e) => e.keyCode === KeyCode.Escape && this.stop())); } public dispose(): void { @@ -222,13 +224,13 @@ class InspectTokensWidget extends Disposable implements IContentWidget { result += `
`; - let metadata = this._decodeMetadata(data.tokens2[(token2Index << 1) + 1]); + let metadata = (token2Index << 1) + 1 < data.tokens2.length ? this._decodeMetadata(data.tokens2[(token2Index << 1) + 1]) : null; result += ``; - result += ``; - result += ``; - result += ``; - result += ``; - result += ``; + result += ``; + result += ``; + result += ``; + result += ``; + result += ``; result += ``; result += `
`; diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 7f1e39821e7..504f3d2356f 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -124,6 +124,13 @@ export interface IGlobalEditorOptions { * Defaults to 20000. */ maxTokenizationLineLength?: number; + /** + * Theme to be used for rendering. + * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black'. + * You can create custom themes via `monaco.editor.defineTheme`. + * To switch a theme, use `monaco.editor.setTheme` + */ + theme?: string; } /** @@ -334,6 +341,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon private readonly _contextViewService: ContextViewService; private readonly _configurationService: IConfigurationService; + private readonly _standaloneThemeService: IStandaloneThemeService; private _ownsModel: boolean; constructor( @@ -363,6 +371,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon this._contextViewService = contextViewService; this._configurationService = configurationService; + this._standaloneThemeService = themeService; this._register(toDispose); this._register(themeDomRegistration); @@ -391,6 +400,9 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon public updateOptions(newOptions: IEditorOptions & IGlobalEditorOptions): void { applyConfigurationValues(this._configurationService, newOptions, false); + if (typeof newOptions.theme === 'string') { + this._standaloneThemeService.setTheme(newOptions.theme); + } super.updateOptions(newOptions); } @@ -414,6 +426,7 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon private readonly _contextViewService: ContextViewService; private readonly _configurationService: IConfigurationService; + private readonly _standaloneThemeService: IStandaloneThemeService; constructor( domElement: HTMLElement, @@ -443,6 +456,7 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon this._contextViewService = contextViewService; this._configurationService = configurationService; + this._standaloneThemeService = themeService; this._register(toDispose); this._register(themeDomRegistration); @@ -454,8 +468,11 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon super.dispose(); } - public updateOptions(newOptions: IDiffEditorOptions): void { + public updateOptions(newOptions: IDiffEditorOptions & IGlobalEditorOptions): void { applyConfigurationValues(this._configurationService, newOptions, true); + if (typeof newOptions.theme === 'string') { + this._standaloneThemeService.setTheme(newOptions.theme); + } super.updateOptions(newOptions); } diff --git a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts index b0b0762997c..cde86790141 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 cfb8396fe37..f3c94bca5e3 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 ff031806fb3..ccd369acc0d 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1108,6 +1108,13 @@ declare namespace monaco.editor { * Defaults to 20000. */ maxTokenizationLineLength?: number; + /** + * Theme to be used for rendering. + * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black'. + * You can create custom themes via `monaco.editor.defineTheme`. + * To switch a theme, use `monaco.editor.setTheme` + */ + theme?: string; } /** @@ -2390,6 +2397,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 +3339,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 +3396,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 +5260,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 25a4db0972c..c30c37e1394 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/configuration/node/configurationService.ts b/src/vs/platform/configuration/common/configurationService.ts similarity index 100% rename from src/vs/platform/configuration/node/configurationService.ts rename to src/vs/platform/configuration/common/configurationService.ts diff --git a/src/vs/platform/configuration/test/node/configurationService.test.ts b/src/vs/platform/configuration/test/node/configurationService.test.ts index f2e90b7d245..15b3d915ed6 100644 --- a/src/vs/platform/configuration/test/node/configurationService.test.ts +++ b/src/vs/platform/configuration/test/node/configurationService.test.ts @@ -9,7 +9,7 @@ import * as path from 'vs/base/common/path'; import * as fs from 'fs'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import * as uuid from 'vs/base/common/uuid'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { testFile } from 'vs/base/test/node/utils'; diff --git a/src/vs/workbench/services/userData/common/inMemoryUserDataProvider.ts b/src/vs/platform/files/common/inMemoryFilesystemProvider.ts similarity index 98% rename from src/vs/workbench/services/userData/common/inMemoryUserDataProvider.ts rename to src/vs/platform/files/common/inMemoryFilesystemProvider.ts index 847aae72b9a..ad689bb57fc 100644 --- a/src/vs/workbench/services/userData/common/inMemoryUserDataProvider.ts +++ b/src/vs/platform/files/common/inMemoryFilesystemProvider.ts @@ -66,9 +66,7 @@ export class InMemoryFileSystemProvider extends Disposable implements IFileSyste async readdir(resource: URI): Promise<[string, FileType][]> { const entry = this._lookupAsDirectory(resource, false); let result: [string, FileType][] = []; - for (const [name, child] of entry.entries) { - result.push([name, child.type]); - } + entry.entries.forEach((child, name) => result.push([name, child.type])); return result; } diff --git a/src/vs/platform/instantiation/common/instantiation.ts b/src/vs/platform/instantiation/common/instantiation.ts index 10ff5a25051..7e130cc3963 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 0d852a63de8..e89a52153d2 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 93429296cfe..aff1675b214 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 2b7e3b8b385..576ae6d82fb 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/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 2b47b768fb7..3fde36a7b6f 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -426,6 +426,10 @@ export const minimapError = registerColor('minimap.errorHighlight', { dark: new export const minimapWarning = registerColor('minimap.warningHighlight', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningBorder }, nls.localize('overviewRuleWarning', 'Minimap marker color for warnings.')); export const minimapBackground = registerColor('minimap.background', { dark: null, light: null, hc: null }, nls.localize('minimapBackground', "Minimap background color.")); +export const minimapSliderBackground = registerColor('minimapSlider.background', { light: transparent(scrollbarSliderBackground, 0.5), dark: transparent(scrollbarSliderBackground, 0.5), hc: transparent(scrollbarSliderBackground, 0.5) }, nls.localize('minimapSliderBackground', "Minimap slider background color.")); +export const minimapSliderHoverBackground = registerColor('minimapSlider.hoverBackground', { light: transparent(scrollbarSliderHoverBackground, 0.5), dark: transparent(scrollbarSliderHoverBackground, 0.5), hc: transparent(scrollbarSliderHoverBackground, 0.5) }, nls.localize('minimapSliderHoverBackground', "Minimap slider background color when hovering.")); +export const minimapSliderActiveBackground = registerColor('minimapSlider.activeBackground', { light: transparent(scrollbarSliderActiveBackground, 0.5), dark: transparent(scrollbarSliderActiveBackground, 0.5), hc: transparent(scrollbarSliderActiveBackground, 0.5) }, nls.localize('minimapSliderActiveBackground', "Minimap slider background color when clicked on.")); + export const problemsErrorIconForeground = registerColor('problemsErrorIcon.foreground', { dark: editorErrorForeground, light: editorErrorForeground, hc: editorErrorForeground }, nls.localize('problemsErrorIconForeground', "The color used for the problems error icon.")); export const problemsWarningIconForeground = registerColor('problemsWarningIcon.foreground', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningForeground }, nls.localize('problemsWarningIconForeground', "The color used for the problems warning icon.")); export const problemsInfoIconForeground = registerColor('problemsInfoIcon.foreground', { dark: editorInfoForeground, light: editorInfoForeground, hc: editorInfoForeground }, nls.localize('problemsInfoIconForeground', "The color used for the problems info icon.")); diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 7ebe69a2c9c..e4d1c0ae4f8 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/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 585a64b6377..3fcdefefdf3 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -18,7 +18,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { URI } from 'vs/base/common/uri'; -import { isEqual } from 'vs/base/common/resources'; +import { isEqual, joinPath } from 'vs/base/common/resources'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store'; @@ -120,23 +120,27 @@ export interface IUserData { } export interface IUserDataSyncStore { - url: string; + url: URI; authenticationProviderId: string; } export function getUserDataSyncStore(configurationService: IConfigurationService): IUserDataSyncStore | undefined { - const value = configurationService.getValue(CONFIGURATION_SYNC_STORE_KEY); - return value && value.url && value.authenticationProviderId ? value : undefined; + const value = configurationService.getValue<{ url: string, authenticationProviderId: string }>(CONFIGURATION_SYNC_STORE_KEY); + if (value && value.url && value.authenticationProviderId) { + return { + url: joinPath(URI.parse(value.url), 'v1'), + authenticationProviderId: value.authenticationProviderId + }; + } + return undefined; } export const ALL_RESOURCE_KEYS: ResourceKey[] = ['settings', 'keybindings', 'extensions', 'globalState']; export type ResourceKey = 'settings' | 'keybindings' | 'extensions' | 'globalState'; export interface IUserDataManifest { - settings: string; - keybindings: string; - extensions: string; - globalState: string; + latest?: Record + session: string; } export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index e10ef737943..920d5b44bc9 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -106,7 +106,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ for (const synchroniser of this.synchronisers) { try { - await synchroniser.sync(manifest ? manifest[synchroniser.resourceKey] : undefined); + await synchroniser.sync(manifest && manifest.latest ? manifest.latest[synchroniser.resourceKey] : undefined); } catch (e) { this.handleSyncError(e, synchroniser.source); } @@ -140,10 +140,11 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return synchroniser.accept(content); } - async hasPreviouslySynced(): Promise { + private async hasPreviouslySynced(): Promise { await this.checkEnablement(); for (const synchroniser of this.synchronisers) { if (await synchroniser.hasPreviouslySynced()) { + return true; } } return false; @@ -153,6 +154,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ await this.checkEnablement(); for (const synchroniser of this.synchronisers) { if (await synchroniser.hasLocalData()) { + return true; } } return false; @@ -189,6 +191,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ await this.checkEnablement(); for (const synchroniser of this.synchronisers) { try { + synchroniser.resetLocal(); } catch (e) { this.logService.error(`${synchroniser.source}: ${toErrorMessage(e)}`); this.logService.error(e); diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index 5d17b1de189..5943d11c42c 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -6,7 +6,6 @@ import { Disposable, } from 'vs/base/common/lifecycle'; import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService, SyncSource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync'; import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request'; -import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request'; @@ -33,7 +32,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn throw new Error('No settings sync store url configured.'); } - const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', key, 'latest').toString(); + const url = joinPath(this.userDataSyncStore.url, 'resource', key, 'latest').toString(); const headers: IHeaders = {}; // Disable caching as they are cached by synchronisers headers['Cache-Control'] = 'no-cache'; @@ -65,7 +64,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn throw new Error('No settings sync store url configured.'); } - const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', key).toString(); + const url = joinPath(this.userDataSyncStore.url, 'resource', key).toString(); const headers: IHeaders = { 'Content-Type': 'text/plain' }; if (ref) { headers['If-Match'] = ref; @@ -89,7 +88,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn throw new Error('No settings sync store url configured.'); } - const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', 'latest').toString(); + const url = joinPath(this.userDataSyncStore.url, 'manifest').toString(); const headers: IHeaders = { 'Content-Type': 'application/json' }; const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None); @@ -105,7 +104,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn throw new Error('No settings sync store url configured.'); } - const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource').toString(); + const url = joinPath(this.userDataSyncStore.url, 'resource').toString(); const headers: IHeaders = { 'Content-Type': 'text/plain' }; const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None); diff --git a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts index 3780af3ab57..24475726d8f 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts @@ -5,11 +5,7 @@ import * as assert from 'assert'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; -import { IStringDictionary } from 'vs/base/common/collections'; -import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; -import { FormattingOptions } from 'vs/base/common/jsonFormatter'; -import { URI } from 'vs/base/common/uri'; -import type { IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { TestUserDataSyncUtilService } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; suite('KeybindingsMerge - No Conflicts', () => { @@ -613,7 +609,7 @@ suite('KeybindingsMerge - No Conflicts', () => { }); async function mergeKeybindings(localContent: string, remoteContent: string, baseContent: string | null) { - const userDataSyncUtilService = new MockUserDataSyncUtilService(); + const userDataSyncUtilService = new TestUserDataSyncUtilService(); const formattingOptions = await userDataSyncUtilService.resolveFormattingOptions(); return merge(localContent, remoteContent, baseContent, formattingOptions, userDataSyncUtilService); } @@ -621,22 +617,3 @@ async function mergeKeybindings(localContent: string, remoteContent: string, bas function stringify(value: any): string { return JSON.stringify(value, null, '\t'); } - -class MockUserDataSyncUtilService implements IUserDataSyncUtilService { - - _serviceBrand: any; - - async resolveUserBindings(userbindings: string[]): Promise> { - const keys: IStringDictionary = {}; - for (const keybinding of userbindings) { - keys[keybinding] = keybinding; - } - return keys; - } - - async resolveFormattingOptions(file?: URI): Promise { - return { eol: '\n', insertSpaces: false, tabSize: 4 }; - } - - async ignoreExtensionsToSync(extensions: IExtensionIdentifier[]): Promise { } -} diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts new file mode 100644 index 00000000000..baf1c1710c1 --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -0,0 +1,239 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRequestService } from 'vs/platform/request/common/request'; +import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IUserData, ResourceKey, IUserDataManifest, ALL_RESOURCE_KEYS, IUserDataAuthTokenService, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncEnablementService, ISettingsSyncService, IUserDataSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { relative } from 'vs/base/common/path'; +import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; +import { generateUuid } from 'vs/base/common/uuid'; +import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { NullLogService, ILogService } from 'vs/platform/log/common/log'; +import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { Schemas } from 'vs/base/common/network'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; +import { URI } from 'vs/base/common/uri'; +import { joinPath } from 'vs/base/common/resources'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { FormattingOptions } from 'vs/base/common/jsonFormatter'; +import { UserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSyncEnablementService'; +import { IGlobalExtensionEnablementService, IExtensionManagementService, IExtensionGalleryService, DidInstallExtensionEvent, DidUninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; +import { Emitter } from 'vs/base/common/event'; + +export class UserDataSyncClient extends Disposable { + + readonly instantiationService: TestInstantiationService; + + constructor(readonly testServer: UserDataSyncTestServer = new UserDataSyncTestServer()) { + super(); + this.instantiationService = new TestInstantiationService(); + } + + async setUp(): Promise { + const userDataDirectory = URI.file('userdata').with({ scheme: Schemas.inMemory }); + const userDataSyncHome = joinPath(userDataDirectory, '.sync'); + const environmentService = this.instantiationService.stub(IEnvironmentService, >{ + userDataSyncHome, + settingsResource: joinPath(userDataDirectory, 'settings.json'), + settingsSyncPreviewResource: joinPath(userDataSyncHome, 'settings.json'), + keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'), + keybindingsSyncPreviewResource: joinPath(userDataSyncHome, 'keybindings.json'), + argvResource: joinPath(userDataDirectory, 'argv.json'), + }); + + const logService = new NullLogService(); + this.instantiationService.stub(ILogService, logService); + + const fileService = this._register(new FileService(logService)); + fileService.registerProvider(Schemas.inMemory, new InMemoryFileSystemProvider()); + this.instantiationService.stub(IFileService, fileService); + + this.instantiationService.stub(IStorageService, new InMemoryStorageService()); + + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ + 'configurationSync.store': { + url: this.testServer.url, + authenticationProviderId: 'test' + } + }))); + await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([]))); + + const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); + await configurationService.initialize(); + this.instantiationService.stub(IConfigurationService, configurationService); + + this.instantiationService.stub(IRequestService, this.testServer); + this.instantiationService.stub(IUserDataAuthTokenService, >{ + onDidChangeToken: new Emitter().event, + async getToken() { return 'token'; } + }); + + this.instantiationService.stub(IUserDataSyncLogService, logService); + this.instantiationService.stub(ITelemetryService, NullTelemetryService); + this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService)); + this.instantiationService.stub(IUserDataSyncUtilService, new TestUserDataSyncUtilService()); + this.instantiationService.stub(IUserDataSyncEnablementService, this.instantiationService.createInstance(UserDataSyncEnablementService)); + + this.instantiationService.stub(IGlobalExtensionEnablementService, this.instantiationService.createInstance(GlobalExtensionEnablementService)); + this.instantiationService.stub(IExtensionManagementService, >{ + async getInstalled() { return []; }, + onDidInstallExtension: new Emitter().event, + onDidUninstallExtension: new Emitter().event, + }); + this.instantiationService.stub(IExtensionGalleryService, >{ + isEnabled() { return true; }, + async getCompatibleExtension() { return null; } + }); + + this.instantiationService.stub(ISettingsSyncService, this.instantiationService.createInstance(SettingsSynchroniser)); + this.instantiationService.stub(IUserDataSyncService, this.instantiationService.createInstance(UserDataSyncService)); + } + +} + +export class UserDataSyncTestServer implements IRequestService { + + _serviceBrand: any; + + readonly url: string = 'http://host:3000'; + private session: string | null = null; + private readonly data: Map = new Map(); + + private _requests: { url: string, type: string, headers?: IHeaders }[] = []; + get requests(): { url: string, type: string, headers?: IHeaders }[] { return this._requests; } + + private _responses: { status: number }[] = []; + get responses(): { status: number }[] { return this._responses; } + reset(): void { this._requests = []; this._responses = []; } + + async resolveProxy(url: string): Promise { return url; } + + async request(options: IRequestOptions, token: CancellationToken): Promise { + const headers: IHeaders = {}; + if (options.headers) { + if (options.headers['If-None-Match']) { + headers['If-None-Match'] = options.headers['If-None-Match']; + } + if (options.headers['If-Match']) { + headers['If-Match'] = options.headers['If-Match']; + } + } + this._requests.push({ url: options.url!, type: options.type!, headers }); + const requestContext = await this.doRequest(options); + this._responses.push({ status: requestContext.res.statusCode! }); + return requestContext; + } + + private async doRequest(options: IRequestOptions): Promise { + const relativePath = relative(`${this.url}/v1/`, options.url!); + const segments = relativePath ? relativePath.split('/') : []; + if (options.type === 'GET' && segments.length === 1 && segments[0] === 'manifest') { + return this.getManifest(options.headers); + } + if (options.type === 'GET' && segments.length === 3 && segments[0] === 'resource' && segments[2] === 'latest') { + return this.getLatestData(segments[1], options.headers); + } + if (options.type === 'POST' && segments.length === 2 && segments[0] === 'resource') { + return this.writeData(segments[1], options.data, options.headers); + } + if (options.type === 'DELETE' && segments.length === 1 && segments[0] === 'resource') { + return this.clear(options.headers); + } + return this.toResponse(501); + } + + private async getManifest(headers?: IHeaders): Promise { + if (this.session) { + const latest: Record = Object.create({}); + const manifest: IUserDataManifest = { session: this.session, latest }; + this.data.forEach((value, key) => latest[key] = value.ref); + return this.toResponse(200, { 'Content-Type': 'application/json' }, JSON.stringify(manifest)); + } + return this.toResponse(204); + } + + private async getLatestData(resource: string, headers: IHeaders = {}): Promise { + const resourceKey = ALL_RESOURCE_KEYS.find(key => key === resource); + if (resourceKey) { + const data = this.data.get(resourceKey); + if (!data) { + return this.toResponse(204, { etag: '0' }); + } + if (headers['If-None-Match'] === data.ref) { + return this.toResponse(304); + } + return this.toResponse(200, { etag: data.ref }, data.content || ''); + } + return this.toResponse(204); + } + + private async writeData(resource: string, content: string = '', headers: IHeaders = {}): Promise { + if (!headers['If-Match']) { + return this.toResponse(428); + } + if (!this.session) { + this.session = generateUuid(); + } + const resourceKey = ALL_RESOURCE_KEYS.find(key => key === resource); + if (resourceKey) { + const data = this.data.get(resourceKey); + if (headers['If-Match'] !== (data ? data.ref : '0')) { + return this.toResponse(412); + } + const ref = `${parseInt(data?.ref || '0') + 1}`; + this.data.set(resourceKey, { ref, content }); + return this.toResponse(200, { etag: ref }); + } + return this.toResponse(204); + } + + private async clear(headers?: IHeaders): Promise { + this.data.clear(); + this.session = null; + return this.toResponse(204); + } + + private toResponse(statusCode: number, headers?: IHeaders, data?: string): IRequestContext { + return { + res: { + headers: headers || {}, + statusCode + }, + stream: bufferToStream(VSBuffer.fromString(data || '')) + }; + } +} + +export class TestUserDataSyncUtilService implements IUserDataSyncUtilService { + + _serviceBrand: any; + + async resolveUserBindings(userbindings: string[]): Promise> { + const keys: IStringDictionary = {}; + for (const keybinding of userbindings) { + keys[keybinding] = keybinding; + } + return keys; + } + + async resolveFormattingOptions(file?: URI): Promise { + return { eol: '\n', insertSpaces: false, tabSize: 4 }; + } + +} + diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts new file mode 100644 index 00000000000..d2c35f09d00 --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -0,0 +1,374 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { IUserDataSyncService, UserDataSyncError, UserDataSyncErrorCode } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { VSBuffer } from 'vs/base/common/buffer'; + +suite('UserDataSyncService', () => { + + const disposableStore = new DisposableStore(); + + teardown(() => disposableStore.clear()); + + test('test first time sync ever', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncService); + + // Sync for first time + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Settings + { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } }, + // Keybindings + { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, + // Extensions + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } } + ]); + + }); + + test('test first time sync from the client with no changes - pull', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the first client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Setup the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + + // Sync (pull) from the test client + target.reset(); + await testObject.isFirstTimeSyncWithMerge(); + await testObject.pull(); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Settings + { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, + // Keybindings + { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, + // Extensions + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + ]); + + }); + + test('test first time sync from the client with changes - pull', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the first client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Setup the test client with changes + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + const fileService = testClient.instantiationService.get(IFileService); + const environmentService = testClient.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); + await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); + await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); + + // Sync (pull) from the test client + target.reset(); + await testObject.isFirstTimeSyncWithMerge(); + await testObject.pull(); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Settings + { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, + // Keybindings + { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, + // Extensions + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + ]); + + }); + + test('test first time sync from the client with no changes - merge', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the first client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Setup the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + + // Sync (merge) from the test client + target.reset(); + await testObject.isFirstTimeSyncWithMerge(); + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Settings + { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, + // Keybindings + { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, + // Extensions + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + ]); + + }); + + test('test first time sync from the client with changes - merge', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the first client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Setup the test client with changes + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const fileService = testClient.instantiationService.get(IFileService); + const environmentService = testClient.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); + await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); + await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + + // Sync (merge) from the test client + target.reset(); + await testObject.isFirstTimeSyncWithMerge(); + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Settings + { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } }, + // Keybindings + { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } }, + // Extensions + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + ]); + + }); + + test('test sync when there are no changes', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + + // sync from the client again + target.reset(); + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + ]); + }); + + test('test sync when there are local changes', async () => { + const target = new UserDataSyncTestServer(); + + // Setup and sync from the client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + target.reset(); + + // Do changes in the client + const fileService = client.instantiationService.get(IFileService); + const environmentService = client.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); + await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); + await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); + + // Sync from the client + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Settings + { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } }, + // Keybindings + { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } }, + // Global state + { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } }, + ]); + }); + + test('test sync when there are remote changes', async () => { + const target = new UserDataSyncTestServer(); + + // Sync from first client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Sync from test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + + // Do changes in first client and sync + const fileService = client.instantiationService.get(IFileService); + const environmentService = client.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); + await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); + await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Sync from test client + target.reset(); + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Settings + { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: { 'If-None-Match': '1' } }, + // Keybindings + { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: { 'If-None-Match': '1' } }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: { 'If-None-Match': '1' } }, + ]); + + }); + + test('test delete', async () => { + const target = new UserDataSyncTestServer(); + + // Sync from the client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + + // Reset from the client + target.reset(); + await testObject.reset(); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'DELETE', url: `${target.url}/v1/resource`, headers: {} }, + ]); + + }); + + test('test delete and sync', async () => { + const target = new UserDataSyncTestServer(); + + // Sync from the client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + + // Reset from the client + await testObject.reset(); + + // Sync again + target.reset(); + await testObject.sync(); + + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Settings + { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } }, + // Keybindings + { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, + // Extensions + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } } + ]); + + }); + + test('test delete on one client throws error on other client while syncing', async () => { + const target = new UserDataSyncTestServer(); + + // Set up and sync from the client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await client.instantiationService.get(IUserDataSyncService).sync(); + + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject = testClient.instantiationService.get(IUserDataSyncService); + await testObject.sync(); + + // Reset from the first client + await client.instantiationService.get(IUserDataSyncService).reset(); + + // Sync from the test client + target.reset(); + try { + await testObject.sync(); + } catch (e) { + assert.ok(e instanceof UserDataSyncError); + assert.deepEqual((e).code, UserDataSyncErrorCode.TurnedOff); + assert.deepEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + ]); + return; + } + throw assert.fail('Should fail with turned off error'); + }); + +}); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 0d0a92fbccc..637dff30817 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 @@ -1430,7 +1488,7 @@ declare module 'vscode' { label: string; /** - * A human-readable string which is rendered less prominent in the same line. + * A human-readable string which is rendered less prominent on the same line. */ description?: string; @@ -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/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index bbe5edd3b6b..bc2a5d3e881 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -14,12 +14,13 @@ import * as modes from 'vs/editor/common/modes'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { Extensions as PanelExtensions, PanelDescriptor, PanelRegistry } from 'vs/workbench/browser/panel'; import { ICommentInfo, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; -import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsPanel'; -import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; +import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView'; import { CommentProviderFeatures, ExtHostCommentsShape, ExtHostContext, IExtHostContext, MainContext, MainThreadCommentsShape, CommentThreadChanges } from '../common/extHost.protocol'; -import { COMMENTS_PANEL_ID, COMMENTS_PANEL_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; +import { COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; +import { ViewContainer, IViewContainersRegistry, Extensions as ViewExtensions, ViewContainerLocation, IViewsRegistry, IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; export class MainThreadCommentThread implements modes.CommentThread { @@ -343,13 +344,14 @@ export class MainThreadComments extends Disposable implements MainThreadComments private _activeCommentThread?: MainThreadCommentThread; private readonly _activeCommentThreadDisposables = this._register(new DisposableStore()); - private _openPanelListener: IDisposable | null = null; + private _openViewListener: IDisposable | null = null; constructor( extHostContext: IExtHostContext, @ICommentService private readonly _commentService: ICommentService, - @IPanelService private readonly _panelService: IPanelService + @IViewsService private readonly _viewsService: IViewsService, + @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostComments); @@ -376,10 +378,10 @@ export class MainThreadComments extends Disposable implements MainThreadComments this._commentService.registerCommentController(providerId, provider); this._commentControllers.set(handle, provider); - const commentsPanelAlreadyConstructed = this._panelService.getPanels().some(panel => panel.id === COMMENTS_PANEL_ID); + const commentsPanelAlreadyConstructed = !!this._viewDescriptorService.getViewDescriptor(COMMENTS_VIEW_ID); if (!commentsPanelAlreadyConstructed) { - this.registerPanel(commentsPanelAlreadyConstructed); - this.registerOpenPanelListener(commentsPanelAlreadyConstructed); + this.registerView(commentsPanelAlreadyConstructed); + this.registerViewOpenedListener(commentsPanelAlreadyConstructed); } this._commentService.setWorkspaceComments(String(handle), []); } @@ -444,27 +446,36 @@ export class MainThreadComments extends Disposable implements MainThreadComments return provider.deleteCommentThread(commentThreadHandle); } - private registerPanel(commentsPanelAlreadyConstructed: boolean) { - if (!commentsPanelAlreadyConstructed) { - Registry.as(PanelExtensions.Panels).registerPanel(PanelDescriptor.create( - CommentsPanel, - COMMENTS_PANEL_ID, - COMMENTS_PANEL_TITLE, - 'commentsPanel', - 10 - )); + private registerView(commentsViewAlreadyRegistered: boolean) { + if (!commentsViewAlreadyRegistered) { + const VIEW_CONTAINER: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ + id: COMMENTS_VIEW_ID, + name: COMMENTS_VIEW_TITLE, + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + order: 10, + }, ViewContainerLocation.Panel); + + Registry.as(ViewExtensions.ViewsRegistry).registerViews([{ + id: COMMENTS_VIEW_ID, + name: COMMENTS_VIEW_TITLE, + canToggleVisibility: false, + ctorDescriptor: new SyncDescriptor(CommentsPanel), + focusCommand: { + id: 'workbench.action.focusCommentsPanel' + } + }], VIEW_CONTAINER); } } /** - * If the comments panel has never been opened, the constructor for it has not yet run so it has - * no listeners for comment threads being set or updated. Listen for the panel opening for the + * If the comments view has never been opened, the constructor for it has not yet run so it has + * no listeners for comment threads being set or updated. Listen for the view opening for the * first time and send it comments then. */ - private registerOpenPanelListener(commentsPanelAlreadyConstructed: boolean) { - if (!commentsPanelAlreadyConstructed && !this._openPanelListener) { - this._openPanelListener = this._panelService.onDidPanelOpen(e => { - if (e.panel.getId() === COMMENTS_PANEL_ID) { + private registerViewOpenedListener(commentsPanelAlreadyConstructed: boolean) { + if (!commentsPanelAlreadyConstructed && !this._openViewListener) { + this._openViewListener = this._viewsService.onDidChangeViewVisibility(e => { + if (e.id === COMMENTS_VIEW_ID && e.visible) { keys(this._commentControllers).forEach(handle => { let threads = this._commentControllers.get(handle)!.getAllComments(); @@ -474,9 +485,9 @@ export class MainThreadComments extends Disposable implements MainThreadComments } }); - if (this._openPanelListener) { - this._openPanelListener.dispose(); - this._openPanelListener = null; + if (this._openViewListener) { + this._openViewListener.dispose(); + this._openViewListener = null; } } }); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 678a44ca3f3..f51e0aaedfb 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -11,7 +11,7 @@ import * as search from 'vs/workbench/contrib/search/common/search'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Position as EditorPosition } from 'vs/editor/common/core/position'; import { Range as EditorRange, IRange } from 'vs/editor/common/core/range'; -import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, IDefinitionLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField, ICodeActionProviderMetadataDto } from '../common/extHost.protocol'; +import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, IDefinitionLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField, ICodeActionProviderMetadataDto, ILanguageWordDefinitionDto } from '../common/extHost.protocol'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -36,6 +36,34 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostLanguageFeatures); this._modeService = modeService; + + if (this._modeService) { + const updateAllWordDefinitions = () => { + const langWordPairs = LanguageConfigurationRegistry.getWordDefinitions(); + let wordDefinitionDtos: ILanguageWordDefinitionDto[] = []; + for (const [languageId, wordDefinition] of langWordPairs) { + const language = this._modeService.getLanguageIdentifier(languageId); + if (!language) { + continue; + } + wordDefinitionDtos.push({ + languageId: language.language, + regexSource: wordDefinition.source, + regexFlags: wordDefinition.flags + }); + } + this._proxy.$setWordDefinitions(wordDefinitionDtos); + }; + LanguageConfigurationRegistry.onDidChange((e) => { + const wordDefinition = LanguageConfigurationRegistry.getWordDefinition(e.languageIdentifier.id); + this._proxy.$setWordDefinitions([{ + languageId: e.languageIdentifier.language, + regexSource: wordDefinition.source, + regexFlags: wordDefinition.flags + }]); + }); + updateAllWordDefinitions(); + } } dispose(): void { @@ -213,6 +241,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 3cd807c4cce..ce7aa7a7b53 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, env: { reason: SaveReason; }): Promise { - - const cts = new CancellationTokenSource(); - - 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, env, 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/mainThreadSearch.ts b/src/vs/workbench/api/browser/mainThreadSearch.ts index 426f361e786..9d2fc19ab3a 100644 --- a/src/vs/workbench/api/browser/mainThreadSearch.ts +++ b/src/vs/workbench/api/browser/mainThreadSearch.ts @@ -80,10 +80,14 @@ class SearchOperation { } addMatch(match: IFileMatch): void { - if (this.matches.has(match.resource.toString())) { - // Merge with previous IFileMatches + const existingMatch = this.matches.get(match.resource.toString()); + if (existingMatch) { // TODO@rob clean up text/file result types - this.matches.get(match.resource.toString())!.results!.push(...match.results!); + // If a file search returns the same file twice, we would enter this branch. + // It's possible that could happen, #90813 + if (existingMatch.results && match.results) { + existingMatch.results.push(...match.results); + } } else { this.matches.set(match.resource.toString(), match); } diff --git a/src/vs/workbench/api/browser/mainThreadTimeline.ts b/src/vs/workbench/api/browser/mainThreadTimeline.ts index 919e4a18ff5..428bf0ed2d9 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 3952717d220..0d25c1176e7 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 c181215c41d..ee4368dbe33 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 @@ -1198,6 +1197,12 @@ export interface IOutgoingCallDto { to: ICallHierarchyItemDto; } +export interface ILanguageWordDefinitionDto { + languageId: string; + regexSource: string; + regexFlags: string +} + export interface ExtHostLanguageFeaturesShape { $provideDocumentSymbols(handle: number, resource: UriComponents, token: CancellationToken): Promise; $provideCodeLenses(handle: number, resource: UriComponents, token: CancellationToken): Promise; @@ -1208,6 +1213,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; @@ -1239,6 +1245,7 @@ export interface ExtHostLanguageFeaturesShape { $provideCallHierarchyIncomingCalls(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $provideCallHierarchyOutgoingCalls(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $releaseCallHierarchy(handle: number, sessionId: string): void; + $setWordDefinitions(wordDefinitions: ILanguageWordDefinitionDto[]): void; } export interface ExtHostQuickOpenShape { @@ -1457,7 +1464,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/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 16f5e3bca7a..0adf5382b3b 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -194,7 +194,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio } catch (err) { // TODO: write to log once we have one } - await allPromises; + await Promise.all(allPromises); } public isActivated(extensionId: ExtensionIdentifier): boolean { diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index b90e3ddeb24..c0522477836 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 { @@ -1883,4 +1916,10 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._proxy.$setLanguageConfiguration(handle, languageId, serializedConfiguration); return this._createDisposable(handle); } + + $setWordDefinitions(wordDefinitions: extHostProtocol.ILanguageWordDefinitionDto[]): void { + for (const wordDefinition of wordDefinitions) { + this._documents.setWordDefinitionFor(wordDefinition.languageId, new RegExp(wordDefinition.regexSource, wordDefinition.regexFlags)); + } + } } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 16000c4f0c9..9d81c5b30cb 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -13,6 +13,7 @@ import { ITerminalChildProcess, ITerminalDimensions, EXT_HOST_CREATION_DELAY } f import { timeout } from 'vs/base/common/async'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape { @@ -290,6 +291,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ protected _activeTerminal: ExtHostTerminal | undefined; protected _terminals: ExtHostTerminal[] = []; protected _terminalProcesses: { [id: number]: ITerminalChildProcess } = {}; + protected _terminalProcessDisposables: { [id: number]: IDisposable } = {}; protected _extensionTerminalAwaitingStart: { [id: number]: { initialDimensions: ITerminalDimensionsDto | undefined } | undefined } = {}; protected _getTerminalPromises: { [id: number]: Promise } = {}; @@ -332,7 +334,10 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ public createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, options, options.name); const p = new ExtHostPseudoterminal(options.pty); - terminal.createExtensionTerminal().then(id => this._setupExtHostProcessListeners(id, p)); + terminal.createExtensionTerminal().then(id => { + const disposable = this._setupExtHostProcessListeners(id, p); + this._terminalProcessDisposables[id] = disposable; + }); this._terminals.push(terminal); return terminal; } @@ -343,7 +348,8 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ throw new Error(`Cannot resolve terminal with id ${id} for virtual process`); } const p = new ExtHostPseudoterminal(pty); - this._setupExtHostProcessListeners(id, p); + const disposable = this._setupExtHostProcessListeners(id, p); + this._terminalProcessDisposables[id] = disposable; } public async $acceptActiveTerminalChanged(id: number | null): Promise { @@ -474,16 +480,18 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ } - protected _setupExtHostProcessListeners(id: number, p: ITerminalChildProcess): void { - p.onProcessReady((e: { pid: number, cwd: string }) => this._proxy.$sendProcessReady(id, e.pid, e.cwd)); - p.onProcessTitleChanged(title => this._proxy.$sendProcessTitle(id, title)); + protected _setupExtHostProcessListeners(id: number, p: ITerminalChildProcess): IDisposable { + const disposables = new DisposableStore(); + + disposables.add(p.onProcessReady((e: { pid: number, cwd: string }) => this._proxy.$sendProcessReady(id, e.pid, e.cwd))); + disposables.add(p.onProcessTitleChanged(title => this._proxy.$sendProcessTitle(id, title))); // Buffer data events to reduce the amount of messages going to the renderer this._bufferer.startBuffering(id, p.onProcessData); - p.onProcessExit(exitCode => this._onProcessExit(id, exitCode)); + disposables.add(p.onProcessExit(exitCode => this._onProcessExit(id, exitCode))); if (p.onProcessOverrideDimensions) { - p.onProcessOverrideDimensions(e => this._proxy.$sendOverrideDimensions(id, e)); + disposables.add(p.onProcessOverrideDimensions(e => this._proxy.$sendOverrideDimensions(id, e))); } this._terminalProcesses[id] = p; @@ -492,6 +500,8 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ p.startSendingEvents(awaitingStart.initialDimensions); delete this._extensionTerminalAwaitingStart[id]; } + + return disposables; } public $acceptProcessInput(id: number, data: string): void { @@ -532,6 +542,13 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ delete this._terminalProcesses[id]; delete this._extensionTerminalAwaitingStart[id]; + // Clean up process disposables + const processDiposable = this._terminalProcessDisposables[id]; + if (processDiposable) { + processDiposable.dispose(); + delete this._terminalProcessDisposables[id]; + } + // Send exit event to main side this._proxy.$sendProcessExit(id, exitCode); } diff --git a/src/vs/workbench/api/common/extHostTimeline.ts b/src/vs/workbench/api/common/extHostTimeline.ts index 1ce8f7813ff..f5f63a2ce4d 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 72c238ee6ba..6027377e166 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 f9f9de26758..f6871d7e2bd 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 c79a3c16ab0..ab474c9825f 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/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index ee2d2c45bb1..dab72ef96b4 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -96,10 +96,8 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { // shellArgs: this._terminalService._getDefaultShellArgs(configProvider), cwd: args.cwd, name: args.title || nls.localize('debug.terminal.title', "debuggee"), - env: args.env }; delete args.cwd; - delete args.env; this._integratedTerminalInstance = this._terminalService.createTerminalFromOptions(options); } diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 5c87849e9ea..bcbae8f4e2d 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 f94fe18534f..66320713617 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 1e07ec23451..cec01038567 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 4f824d94c74..4ed709af080 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 583bdb727a7..19781fbb59f 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/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index da914ba0b90..2012c00b543 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -55,6 +55,7 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { EditorAutoSave } from 'vs/workbench/browser/parts/editor/editorAutoSave'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; // Register String Editor Registry.as(EditorExtensions.Editors).registerEditor( @@ -134,9 +135,21 @@ class UntitledTextEditorInputFactory implements IEditorInputFactory { resource = toLocalResource(resource, this.environmentService.configuration.remoteAuthority); // untitled with associated file path use the local schema } + // Mode: only remember mode if it is either specific (not text) + // or if the mode was explicitly set by the user. We want to preserve + // this information across restarts and not set the mode unless + // this is the case. + let modeId: string | undefined; + const modeIdCandidate = untitledTextEditorInput.getMode(); + if (modeIdCandidate !== PLAINTEXT_MODE_ID) { + modeId = modeIdCandidate; + } else if (untitledTextEditorInput.model.hasModeSetExplicitly) { + modeId = modeIdCandidate; + } + const serialized: ISerializedUntitledTextEditorInput = { resourceJSON: resource.toJSON(), - modeId: untitledTextEditorInput.getMode(), + modeId, encoding: untitledTextEditorInput.getEncoding() }; diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 11f5d5a0a04..cb05c88239b 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/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index a04814d2019..3909f3aa753 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -214,6 +214,10 @@ export class TextResourceEditor extends AbstractTextResourceEditor { } private onDidEditorPaste(e: IPasteEvent, codeEditor: ICodeEditor): void { + if (this.input instanceof UntitledTextEditorInput && this.input.model.hasModeSetExplicitly) { + return; // do not override mode if it was set explicitly + } + if (e.range.startLineNumber !== 1 || e.range.startColumn !== 1) { return; // only when pasting into first line, first column (= empty document) } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts b/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts index 94a5a660b43..fae7f87f7cb 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 6d27e856dbb..0b4cbe77264 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/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index dbf6f3adbe5..133bb8ff3ab 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -222,4 +222,5 @@ export function registerNotificationCommands(center: INotificationsCenterControl MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: SHOW_NOTIFICATIONS_CENTER, title: { value: localize('showNotifications', "Show Notifications"), original: 'Show Notifications' }, category }, when: NotificationsCenterVisibleContext.toNegated() }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: HIDE_NOTIFICATIONS_CENTER, title: { value: localize('hideNotifications', "Hide Notifications"), original: 'Hide Notifications' }, category }, when: NotificationsCenterVisibleContext }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: CLEAR_ALL_NOTIFICATIONS, title: { value: localize('clearAllNotifications', "Clear All Notifications"), original: 'Clear All Notifications' }, category } }); -} \ No newline at end of file + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: FOCUS_NOTIFICATION_TOAST, title: { value: localize('focusNotificationToasts', "Focus Notification Toast"), original: 'Focus Notification Toast' }, category }, when: NotificationsToastsVisibleContext }); +} diff --git a/src/vs/workbench/browser/parts/notifications/notificationsList.ts b/src/vs/workbench/browser/parts/notifications/notificationsList.ts index ae4d2e69aff..2d585698cb9 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 97c9af2a622..3545ab9c02b 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 ee6bbffc283..9461b4887ac 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 a41db2f2ee1..318307a8477 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 b1cdfc6f4eb..6ee4f8cff77 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 2e4de06c27b..bd12bd60f9b 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 f50f32cc0ae..074150b643d 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; @@ -88,15 +170,16 @@ export abstract class ViewPane extends Pane implements IView { private readonly menuActions: ViewMenuActions; protected actionRunner?: IActionRunner; - protected toolbar?: ToolBar; + private toolbar?: ToolBar; private readonly showActionsAlways: boolean = false; private headerContainer?: HTMLElement; private titleContainer?: HTMLElement; 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 2ea6bcae00f..ddcb76a0a95 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/web.main.ts b/src/vs/workbench/browser/web.main.ts index 96de0d2a04d..8973e3fc36c 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -41,7 +41,6 @@ import { joinPath } from 'vs/base/common/resources'; import { BrowserStorageService } from 'vs/platform/storage/browser/storageService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { getThemeTypeSelector, DARK, HIGH_CONTRAST, LIGHT } from 'vs/platform/theme/common/themeService'; -import { InMemoryFileSystemProvider } from 'vs/workbench/services/userData/common/inMemoryUserDataProvider'; import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; import { FileLogService } from 'vs/platform/log/common/fileLogService'; @@ -51,6 +50,7 @@ import { InMemoryLogProvider } from 'vs/workbench/services/log/common/inMemoryLo import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows'; import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; import { coalesce } from 'vs/base/common/arrays'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; class BrowserMain extends Disposable { @@ -93,20 +93,16 @@ class BrowserMain extends Disposable { // Layout const viewport = platform.isIOS && (window).visualViewport ? (window).visualViewport /** Visual viewport */ : window /** Layout viewport */; - this._register(addDisposableListener(viewport, EventType.RESIZE, () => { - workbench.layout(); - })); + this._register(addDisposableListener(viewport, EventType.RESIZE, () => workbench.layout())); // Prevent the back/forward gestures in macOS - this._register(addDisposableListener(this.domElement, EventType.WHEEL, (e) => { - e.preventDefault(); - }, { passive: false })); + this._register(addDisposableListener(this.domElement, EventType.WHEEL, e => e.preventDefault(), { passive: false })); // Prevent native context menus in web - this._register(addDisposableListener(this.domElement, EventType.CONTEXT_MENU, (e) => EventHelper.stop(e, true))); + this._register(addDisposableListener(this.domElement, EventType.CONTEXT_MENU, e => EventHelper.stop(e, true))); // Prevent default navigation on drop - this._register(addDisposableListener(this.domElement, EventType.DROP, (e) => EventHelper.stop(e, true))); + this._register(addDisposableListener(this.domElement, EventType.DROP, e => EventHelper.stop(e, true))); // Workbench Lifecycle this._register(workbench.onBeforeShutdown(event => { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 30373ccb986..acb6715b8d3 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -191,7 +191,7 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio 'type': 'string', 'enum': ['left', 'bottom', 'right'], 'default': 'bottom', - 'description': nls.localize('panelDefaultLocation', "Controls the default location of the panel (terminal, debug console, output, problems). It can either show at the bottom or on the right of the workbench.") + 'description': nls.localize('panelDefaultLocation', "Controls the default location of the panel (terminal, debug console, output, problems). It can either show at the bottom, right, or left of the workbench.") }, 'workbench.statusBar.visible': { 'type': 'boolean', diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 604e4d1e914..a24435cd241 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 a6b19e5df2c..ff932b00bd8 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 c509716fc49..408dcca11ac 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 0a6073ca70b..b171e8d1dcd 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 2d0de4bbb3d..0970caecdbb 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 8d7ec503497..06bdb85376a 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 4a50a0b6b89..544fa1027d4 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 dc8c179721c..49cf081d967 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 00000000000..ea6fc68420f --- /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 e9c8770111e..c695bd3eaef 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/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index ba8510c41f4..f047707083b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -22,8 +22,8 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; -export const COMMENTS_PANEL_ID = 'workbench.panel.comments'; -export const COMMENTS_PANEL_TITLE = 'Comments'; +export const COMMENTS_VIEW_ID = 'workbench.panel.comments'; +export const COMMENTS_VIEW_TITLE = 'Comments'; export class CommentsAsyncDataSource implements IAsyncDataSource { hasChildren(element: any): boolean { @@ -176,7 +176,7 @@ export class CommentsList extends WorkbenchAsyncDataTree { renderers, dataSource, { - ariaLabel: COMMENTS_PANEL_TITLE, + ariaLabel: COMMENTS_VIEW_TITLE, keyboardSupport: true, identityProvider: { getId: (element: any) => { diff --git a/src/vs/workbench/contrib/comments/browser/commentsPanel.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts similarity index 76% rename from src/vs/workbench/contrib/comments/browser/commentsPanel.ts rename to src/vs/workbench/contrib/comments/browser/commentsView.ts index 923fb4dcd0b..ebd2b1ae5de 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsPanel.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -11,22 +11,25 @@ import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TreeResourceNavigator } from 'vs/platform/list/browser/listService'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { Panel } from 'vs/workbench/browser/panel'; import { CommentNode, CommentsModel, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { CommentController } from 'vs/workbench/contrib/comments/browser/commentsEditorContribution'; -import { ICommentService, IWorkspaceCommentThreadsEvent } from 'vs/workbench/contrib/comments/browser/commentService'; +import { IWorkspaceCommentThreadsEvent, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IStorageService } from 'vs/platform/storage/common/storage'; import { ResourceLabels } from 'vs/workbench/browser/labels'; -import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { CommentsList, COMMENTS_PANEL_ID, COMMENTS_PANEL_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; +import { CommentsList, COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; -export class CommentsPanel extends Panel { +export class CommentsPanel extends ViewPane { private treeLabels!: ResourceLabels; private tree!: CommentsList; private treeContainer!: HTMLElement; @@ -35,43 +38,50 @@ export class CommentsPanel extends Panel { private commentsModel!: CommentsModel; private collapseAllAction?: IAction; + readonly onDidChangeVisibility = this.onDidChangeBodyVisibility; + constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, - @ICommentService private readonly commentService: ICommentService, + options: IViewPaneOptions, + @IInstantiationService readonly instantiationService: IInstantiationService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IEditorService private readonly editorService: IEditorService, - @ITelemetryService telemetryService: ITelemetryService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IContextMenuService contextMenuService: IContextMenuService, + @IKeybindingService keybindingService: IKeybindingService, + @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService + @ICommentService private readonly commentService: ICommentService ) { - super(COMMENTS_PANEL_ID, telemetryService, themeService, storageService); + super({ ...(options as IViewPaneOptions), id: COMMENTS_VIEW_ID, ariaHeaderLabel: COMMENTS_VIEW_TITLE }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); } - public create(parent: HTMLElement): void { - super.create(parent); + public renderBody(container: HTMLElement): void { + super.renderBody(container); - dom.addClass(parent, 'comments-panel'); + dom.addClass(container, 'comments-panel'); - let container = dom.append(parent, dom.$('.comments-panel-container')); - this.treeContainer = dom.append(container, dom.$('.tree-container')); + let domContainer = dom.append(container, dom.$('.comments-panel-container')); + this.treeContainer = dom.append(domContainer, dom.$('.tree-container')); this.commentsModel = new CommentsModel(); this.createTree(); - this.createMessageBox(container); + this.createMessageBox(domContainer); this._register(this.commentService.onDidSetAllCommentThreads(this.onAllCommentsChanged, this)); this._register(this.commentService.onDidUpdateCommentThreads(this.onCommentsUpdated, this)); - const styleElement = dom.createStyleSheet(parent); + const styleElement = dom.createStyleSheet(container); this.applyStyles(styleElement); this._register(this.themeService.onThemeChange(_ => this.applyStyles(styleElement))); - this._register(this.onDidChangeVisibility(visible => { + this._register(this.onDidChangeBodyVisibility(visible => { if (visible) { this.refresh(); } })); - this.render(); + this.renderComments(); } private applyStyles(styleElement: HTMLStyleElement) { @@ -101,7 +111,7 @@ export class CommentsPanel extends Panel { styleElement.innerHTML = content.join('\n'); } - private async render(): Promise { + private async renderComments(): Promise { dom.toggleClass(this.treeContainer, 'hidden', !this.commentsModel.hasCommentThreads()); await this.tree.setInput(this.commentsModel); this.renderMessage(); @@ -116,12 +126,12 @@ export class CommentsPanel extends Panel { return [this.collapseAllAction]; } - public layout(dimensions: dom.Dimension): void { - this.tree.layout(dimensions.height, dimensions.width); + public layoutBody(height: number, width: number): void { + this.tree.layout(height, width); } public getTitle(): string { - return COMMENTS_PANEL_TITLE; + return COMMENTS_VIEW_TITLE; } private createMessageBox(parent: HTMLElement): void { @@ -224,10 +234,7 @@ export class CommentsPanel extends Panel { CommandsRegistry.registerCommand({ id: 'workbench.action.focusCommentsPanel', handler: async (accessor) => { - const panelService = accessor.get(IPanelService); - const panels = panelService.getPanels(); - if (panels.some(panelIdentifier => panelIdentifier.id === COMMENTS_PANEL_ID)) { - await panelService.openPanel(COMMENTS_PANEL_ID, true); - } + const viewsService = accessor.get(IViewsService); + viewsService.openView(COMMENTS_VIEW_ID, true); } }); diff --git a/src/vs/workbench/contrib/customEditor/browser/commands.ts b/src/vs/workbench/contrib/customEditor/browser/commands.ts index 6814383e70b..71c474a0b15 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/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 67cff6f0e97..d64f9b7134e 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -28,7 +28,7 @@ import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { ViewPane, 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 { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Gesture } from 'vs/base/browser/touch'; @@ -81,6 +81,7 @@ export class BreakpointsView extends ViewPane { 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); @@ -153,12 +154,6 @@ export class BreakpointsView extends ViewPane { this.onBreakpointsChange(); } })); - - this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { - if (views.some(v => v.id === this.id)) { - this.list.updateOptions({ overrideStyles: { listBackground: this.getBackgroundColor() } }); - } - })); } public focus(): void { diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 6d195c8220f..5ae2c952efe 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -119,16 +119,11 @@ export class CallStackView extends ViewPane { this.pauseMessageLabel.title = thread.stoppedDetails.text || ''; dom.toggleClass(this.pauseMessageLabel, 'exception', thread.stoppedDetails.reason === 'exception'); this.pauseMessage.hidden = false; - if (this.toolbar) { - this.toolbar.setActions([])(); - } + this.updateActions(); } else { this.pauseMessage.hidden = true; - if (this.toolbar) { - const collapseAction = new CollapseAction(this.tree, true, 'explorer-action codicon-collapse-all'); - this.toolbar.setActions([collapseAction])(); - } + this.updateActions(); } this.needsRefresh = false; @@ -153,9 +148,17 @@ export class CallStackView extends ViewPane { this.pauseMessageLabel = dom.append(this.pauseMessage, $('span.label')); } + getActions(): IAction[] { + if (this.pauseMessage.hidden) { + return [new CollapseAction(this.tree, true, 'explorer-action codicon-collapse-all')]; + } + + return []; + } + renderBody(container: HTMLElement): void { super.renderBody(container); - + dom.addClass(this.element, 'debug-pane'); dom.addClass(container, 'debug-call-stack'); const treeContainer = renderViewTree(container); @@ -295,12 +298,6 @@ export class CallStackView extends ViewPane { this.parentSessionToExpand.add(s.parentSession); } })); - - this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { - if (views.some(v => v.id === this.id)) { - this.tree.updateOptions({ overrideStyles: { listBackground: this.getBackgroundColor() } }); - } - })); } layoutBody(height: number, width: number): void { diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 514589c23cf..264fa2a775b 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -72,7 +72,7 @@ const viewContainer = Registry.as(ViewExtensions.ViewCo id: VIEWLET_ID, name: nls.localize('run', "Run"), ctorDescriptor: new SyncDescriptor(DebugViewPaneContainer), - icon: 'codicon-debug-alt', + icon: 'codicon-debug-alt-2', order: 3 }, ViewContainerLocation.Sidebar); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index c22007db2f2..a8cba2d296b 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/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index f6a49d3eeeb..6e863fc5abe 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -117,7 +117,6 @@ export class DebugService implements IDebugService { this.model = new DebugModel(this.loadBreakpoints(), this.loadFunctionBreakpoints(), this.loadExceptionBreakpoints(), this.loadDataBreakpoints(), this.loadWatchExpressions(), this.textFileService); - this.toDispose.push(this.model); const setBreakpointsExistContext = () => this.breakpointsExist.set(!!(this.model.getBreakpoints().length || this.model.getDataBreakpoints().length || this.model.getFunctionBreakpoints().length)); this.breakpointsExist = CONTEXT_BREAKPOINTS_EXIST.bindTo(contextKeyService); setBreakpointsExistContext(); @@ -126,7 +125,7 @@ export class DebugService implements IDebugService { this.taskRunner = this.instantiationService.createInstance(DebugTaskRunner); this.toDispose.push(this.fileService.onFileChanges(e => this.onFileChanges(e))); - this.lifecycleService.onShutdown(this.dispose, this); + this.toDispose.push(this.lifecycleService.onShutdown(this.dispose, this)); this.toDispose.push(this.extensionHostDebugService.onAttachSession(event => { const session = this.model.getSession(event.sessionId, true); @@ -165,7 +164,7 @@ export class DebugService implements IDebugService { this.debugUx.set(!!(this.state !== State.Inactive || this.configurationManager.selectedConfiguration.name) ? 'default' : 'simple'); })); this.toDispose.push(this.model.onDidChangeCallStack(() => { - const numberOfSessions = this.model.getSessions().length; + const numberOfSessions = this.model.getSessions().filter(s => !s.parentSession).length; if (this.activity) { this.activity.dispose(); } @@ -522,7 +521,6 @@ export class DebugService implements IDebugService { await this.focusStackFrame(undefined, undefined, session); } } catch (err) { - session.shutdown(); if (this.viewModel.focusedSession === session) { await this.focusStackFrame(undefined); } @@ -567,7 +565,6 @@ export class DebugService implements IDebugService { this.notificationService.error(err); } } - session.shutdown(); this.endInitializingState(); this._onDidEndSession.fire(session); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 11c7ef8329c..8af3f1ee82f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -33,6 +33,7 @@ import { variableSetEmitter } from 'vs/workbench/contrib/debug/browser/variables import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { distinct } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; export class DebugSession implements IDebugSession { @@ -74,7 +75,8 @@ export class DebugSession implements IDebugSession { @IProductService private readonly productService: IProductService, @IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService, @IOpenerService private readonly openerService: IOpenerService, - @INotificationService private readonly notificationService: INotificationService + @INotificationService private readonly notificationService: INotificationService, + @ILifecycleService lifecycleService: ILifecycleService ) { this.id = generateUuid(); this._options = options || {}; @@ -83,7 +85,15 @@ export class DebugSession implements IDebugSession { } else { this.repl = (this.parentSession as DebugSession).repl; } - this.repl.onDidChangeElements(() => this._onDidChangeREPLElements.fire()); + + const toDispose: IDisposable[] = []; + toDispose.push(this.repl.onDidChangeElements(() => this._onDidChangeREPLElements.fire())); + if (lifecycleService) { + toDispose.push(lifecycleService.onShutdown(() => { + this.shutdown(); + dispose(toDispose); + })); + } } getId(): string { @@ -213,6 +223,7 @@ export class DebugSession implements IDebugSession { } catch (err) { this.initialized = true; this._onDidChangeState.fire(); + this.shutdown(); throw err; } } @@ -227,8 +238,12 @@ export class DebugSession implements IDebugSession { // __sessionID only used for EH debugging (but we add it always for now...) config.__sessionId = this.getId(); - await this.raw.launchOrAttach(config); - + try { + await this.raw.launchOrAttach(config); + } catch (err) { + this.shutdown(); + throw err; + } } /** @@ -892,19 +907,22 @@ export class DebugSession implements IDebugSession { this.rawListeners.push(this.raw.onDidExitAdapter(event => { this.initialized = true; this.model.setBreakpointSessionData(this.getId(), this.capabilities, undefined); + this.shutdown(); this._onDidEndAdapter.fire(event); })); } - shutdown(): void { + // Disconnects and clears state. Session can be initialized again for a new connection. + private shutdown(): void { dispose(this.rawListeners); - if (this.raw) { - this.raw.disconnect(); - this.raw.dispose(); - } - this.raw = undefined; this.fetchThreadsScheduler = undefined; this.model.clearThreads(this.getId(), true); + if (this.raw) { + const raw = this.raw; + this.raw = undefined; + raw.disconnect(); + raw.dispose(); + } this._onDidChangeState.fire(); } diff --git a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts index a0aea70ae0e..231ac807506 100644 --- a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts +++ b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts @@ -72,7 +72,7 @@ export class DebugTaskRunner { await this.viewsService.openView(Constants.MARKERS_VIEW_ID); return Promise.resolve(TaskRunResult.Failure); } - if (onTaskErrors === 'cancel') { + if (onTaskErrors === 'abort') { return Promise.resolve(TaskRunResult.Failure); } @@ -85,7 +85,7 @@ export class DebugTaskRunner { ? nls.localize('preLaunchTaskExitCode', "The preLaunchTask '{0}' terminated with exit code {1}.", taskLabel, taskSummary.exitCode) : nls.localize('preLaunchTaskTerminated', "The preLaunchTask '{0}' terminated.", taskLabel); - const result = await this.dialogService.show(severity.Warning, message, [nls.localize('debugAnyway', "Debug Anyway"), nls.localize('showErrors', "Show Errors"), nls.localize('cancel', "Cancel")], { + const result = await this.dialogService.show(severity.Warning, message, [nls.localize('debugAnyway', "Debug Anyway"), nls.localize('showErrors', "Show Errors"), nls.localize('abort', "Abort")], { checkbox: { label: nls.localize('remember', "Remember my choice in user settings"), }, @@ -94,12 +94,12 @@ export class DebugTaskRunner { const debugAnyway = result.choice === 0; - const cancel = result.choice = 2; + const abort = result.choice === 2; if (result.checkboxChecked) { - this.configurationService.updateValue('debug.onTaskErrors', result.choice === 0 ? 'debugAnyway' : cancel ? 'cancel' : 'showErrors'); + this.configurationService.updateValue('debug.onTaskErrors', result.choice === 0 ? 'debugAnyway' : abort ? 'abort' : 'showErrors'); } - if (cancel) { + if (abort) { return Promise.resolve(TaskRunResult.Failure); } if (debugAnyway) { diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index 82460c27fa4..ceb03abfcc9 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -435,6 +435,7 @@ export class LoadedScriptsView extends ViewPane { 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'); @@ -573,12 +574,6 @@ export class LoadedScriptsView extends ViewPane { } })); - this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { - if (views.some(v => v.id === this.id)) { - this.tree.updateOptions({ overrideStyles: { listBackground: this.getBackgroundColor() } }); - } - })); - // feature: expand all nodes when filtering (not when finding) let viewState: IViewState | undefined; this._register(this.tree.onDidChangeTypeFilterPattern(pattern => { 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 12a93e85e7d..2fb7b93bd76 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 b01a3d1193d..a0406499aa3 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/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index f14030847df..2408b52df02 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -5,7 +5,7 @@ /* Debug viewlet */ -.debug-viewlet { +.debug-pane { height: 100%; } @@ -13,27 +13,27 @@ height: 100%; } -.debug-viewlet .debug-start-view { +.debug-pane .debug-start-view { padding: 0 20px 0 20px; } -.debug-viewlet .debug-start-view .monaco-button, -.debug-viewlet .debug-start-view .section { +.debug-pane .debug-start-view .monaco-button, +.debug-pane .debug-start-view .section { margin-top: 20px; } -.debug-viewlet .debug-start-view .top-section { +.debug-pane .debug-start-view .top-section { margin-top: 10px; } -.debug-viewlet .debug-start-view .monaco-button { +.debug-pane .debug-start-view .monaco-button { max-width: 260px; margin-left: auto; margin-right: auto; display: block; } -.debug-viewlet .debug-start-view .click { +.debug-pane .debug-start-view .click { cursor: pointer; color: #007ACC; } @@ -87,7 +87,7 @@ /* Debug viewlet trees */ -.debug-viewlet .line-number { +.debug-pane .line-number { background: rgba(136, 136, 136, 0.3); border-radius: 2px; font-size: 0.9em; @@ -95,29 +95,29 @@ line-height: 20px; } -.debug-viewlet .monaco-list-row.selected .line-number, -.debug-viewlet .monaco-list-row.selected .thread > .state > .label, -.debug-viewlet .monaco-list-row.selected .session > .state > .label { +.debug-pane .monaco-list-row.selected .line-number, +.debug-pane .monaco-list-row.selected .thread > .state > .label, +.debug-pane .monaco-list-row.selected .session > .state > .label { background-color: #ffffff; color: #666; } -.debug-viewlet .monaco-list:focus .monaco-list-row.selected.focused .codicon { +.debug-pane .monaco-list:focus .monaco-list-row.selected.focused .codicon { color: inherit !important; } -.debug-viewlet .disabled { +.debug-pane .disabled { opacity: 0.65; } /* Call stack */ -.debug-viewlet .debug-call-stack-title { +.debug-pane .debug-call-stack-title { display: flex; width: 100%; } -.debug-viewlet .debug-call-stack-title > .pause-message { +.debug-pane .debug-call-stack-title > .pause-message { flex: 1; text-align: right; text-overflow: ellipsis; @@ -126,41 +126,41 @@ margin: 0px 10px; } -.debug-viewlet .debug-call-stack-title > .pause-message > .label { +.debug-pane .debug-call-stack-title > .pause-message > .label { border-radius: 3px; padding: 1px 2px; font-size: 9px; } -.debug-viewlet .debug-call-stack-title > .pause-message > .label.exception { +.debug-pane .debug-call-stack-title > .pause-message > .label.exception { background-color: #A31515; color: rgb(255, 255, 255); } -.vs-dark .debug-viewlet .debug-call-stack-title > .pause-message > .label.exception { +.vs-dark .debug-pane .debug-call-stack-title > .pause-message > .label.exception { background-color: #6C2022; color: inherit; } -.hc-black .debug-viewlet .debug-call-stack-title > .pause-message > .label.exception { +.hc-black .debug-pane .debug-call-stack-title > .pause-message > .label.exception { background-color: #6C2022; color: inherit; } -.debug-viewlet .debug-call-stack .thread, -.debug-viewlet .debug-call-stack .session { +.debug-pane .debug-call-stack .thread, +.debug-pane .debug-call-stack .session { display: flex; } -.debug-viewlet .debug-call-stack .thread > .name, -.debug-viewlet .debug-call-stack .session > .name { +.debug-pane .debug-call-stack .thread > .name, +.debug-pane .debug-call-stack .session > .name { flex: 1; overflow: hidden; text-overflow: ellipsis; } -.debug-viewlet .debug-call-stack .thread > .state, -.debug-viewlet .debug-call-stack .session > .state { +.debug-pane .debug-call-stack .thread > .state, +.debug-pane .debug-call-stack .session > .state { text-align: right; overflow: hidden; text-overflow: ellipsis; @@ -168,116 +168,116 @@ text-transform: uppercase; } -.debug-viewlet .debug-call-stack .monaco-list-row:hover .state { +.debug-pane .debug-call-stack .monaco-list-row:hover .state { display: none; } -.debug-viewlet .debug-call-stack .monaco-list-row:hover .stack-frame.has-actions .file .line-number { +.debug-pane .debug-call-stack .monaco-list-row:hover .stack-frame.has-actions .file .line-number { display: none; } -.debug-viewlet .debug-call-stack .monaco-list-row .monaco-action-bar { +.debug-pane .debug-call-stack .monaco-list-row .monaco-action-bar { display: none; } -.debug-viewlet .debug-call-stack .monaco-list-row:hover .monaco-action-bar { +.debug-pane .debug-call-stack .monaco-list-row:hover .monaco-action-bar { display: initial; } -.monaco-workbench .debug-viewlet .debug-call-stack .monaco-action-bar .action-item > .action-label { +.monaco-workbench .debug-pane .debug-call-stack .monaco-action-bar .action-item > .action-label { width: 16px; height: 100%; margin-right: 8px; vertical-align: text-top; } -.debug-viewlet .debug-call-stack .thread > .state > .label, -.debug-viewlet .debug-call-stack .session > .state > .label { +.debug-pane .debug-call-stack .thread > .state > .label, +.debug-pane .debug-call-stack .session > .state > .label { background: rgba(136, 136, 136, 0.3); border-radius: 2px; font-size: 0.8em; padding: 0 3px; } -.debug-viewlet .debug-call-stack .stack-frame { +.debug-pane .debug-call-stack .stack-frame { overflow: hidden; text-overflow: ellipsis; padding-right: 0.8em; display: flex; } -.debug-viewlet .debug-call-stack .stack-frame.label { +.debug-pane .debug-call-stack .stack-frame.label { text-align: center; font-style: italic; } -.debug-viewlet .debug-call-stack .stack-frame .label { +.debug-pane .debug-call-stack .stack-frame .label { flex: 1; flex-shrink: 0; min-width: fit-content; min-width: -moz-fit-content; } -.debug-viewlet .debug-call-stack .stack-frame.subtle { +.debug-pane .debug-call-stack .stack-frame.subtle { font-style: italic; } -.debug-viewlet .debug-call-stack .stack-frame.label > .file { +.debug-pane .debug-call-stack .stack-frame.label > .file { display: none; } -.debug-viewlet .debug-call-stack .stack-frame > .file { +.debug-pane .debug-call-stack .stack-frame > .file { display: flex; overflow: hidden; flex-wrap: wrap; justify-content: flex-end; } -.debug-viewlet .debug-call-stack .stack-frame > .file > .line-number.unavailable { +.debug-pane .debug-call-stack .stack-frame > .file > .line-number.unavailable { display: none; } -.debug-viewlet .debug-call-stack .monaco-list-row:not(.selected) .stack-frame > .file { +.debug-pane .debug-call-stack .monaco-list-row:not(.selected) .stack-frame > .file { color: rgba(108, 108, 108, 0.8); } -.debug-viewlet .debug-call-stack .stack-frame > .file > .file-name { +.debug-pane .debug-call-stack .stack-frame > .file > .file-name { overflow: hidden; text-overflow: ellipsis; margin-right: 0.8em; } -.vs-dark .debug-viewlet .debug-call-stack .monaco-list-row:not(.selected) .stack-frame > .file { +.vs-dark .debug-pane .debug-call-stack .monaco-list-row:not(.selected) .stack-frame > .file { color: rgba(204, 204, 204, 0.6); } -.debug-viewlet .debug-call-stack .stack-frame > .file:not(:first-child) { +.debug-pane .debug-call-stack .stack-frame > .file:not(:first-child) { margin-left: 0.8em; } -.debug-viewlet .debug-call-stack .load-more { +.debug-pane .debug-call-stack .load-more { font-style: italic; text-align: center; } -.debug-viewlet .debug-call-stack .show-more { +.debug-pane .debug-call-stack .show-more { font-style: italic; opacity: 0.35; } -.debug-viewlet .debug-call-stack .error { +.debug-pane .debug-call-stack .error { font-style: italic; text-overflow: ellipsis; overflow: hidden; } -.debug-viewlet .debug-call-stack .monaco-list:focus .monaco-list-row.selected .codicon { +.debug-pane .debug-call-stack .monaco-list:focus .monaco-list-row.selected .codicon { color: inherit !important; } /* Variables & Expression view */ -.debug-viewlet .scope { +.debug-pane .scope { font-weight: bold; font-size: 11px; } @@ -295,7 +295,7 @@ 100% { background-color: rgba(86, 156, 214, .2) } } -.debug-viewlet .monaco-list-row .expression .value.changed { +.debug-pane .monaco-list-row .expression .value.changed { padding: 2px; margin: 4px; border-radius: 4px; @@ -305,67 +305,67 @@ animation-fill-mode: forwards; } -.debug-viewlet .monaco-inputbox { +.debug-pane .monaco-inputbox { width: 100%; line-height: normal; } -.debug-viewlet .inputBoxContainer { +.debug-pane .inputBoxContainer { box-sizing: border-box; flex-grow: 1; } -.debug-viewlet .debug-watch .monaco-inputbox { +.debug-pane .debug-watch .monaco-inputbox { font-family: var(--monaco-monospace-font); } -.debug-viewlet .monaco-inputbox > .wrapper { +.debug-pane .monaco-inputbox > .wrapper { height: 19px; } -.debug-viewlet .monaco-inputbox > .wrapper > .input { +.debug-pane .monaco-inputbox > .wrapper > .input { padding: 0px; color: initial; } -.debug-viewlet .watch-expression { +.debug-pane .watch-expression { display: flex; } -.debug-viewlet .watch-expression .expression { +.debug-pane .watch-expression .expression { flex : 1; } -.vs-dark .debug-viewlet .monaco-list-row .expression .value.changed { +.vs-dark .debug-pane .monaco-list-row .expression .value.changed { animation-name: debugViewletValueChanged; } /* Breakpoints */ -.debug-viewlet .monaco-list-row { +.debug-pane .monaco-list-row { line-height: 22px; } -.debug-viewlet .debug-breakpoints .monaco-list-row .breakpoint { +.debug-pane .debug-breakpoints .monaco-list-row .breakpoint { padding-left: 2px; } -.debug-viewlet .debug-breakpoints .breakpoint.exception { +.debug-pane .debug-breakpoints .breakpoint.exception { padding-left: 20px; } -.debug-viewlet .debug-breakpoints .breakpoint { +.debug-pane .debug-breakpoints .breakpoint { display: flex; padding-right: 0.8em; flex: 1; align-items: center; } -.debug-viewlet .debug-breakpoints .breakpoint input { +.debug-pane .debug-breakpoints .breakpoint input { flex-shrink: 0; } -.debug-viewlet .debug-breakpoints .breakpoint > .codicon { +.debug-pane .debug-breakpoints .breakpoint > .codicon { width: 19px; height: 19px; min-width: 19px; @@ -374,7 +374,7 @@ justify-content: center; } -.debug-viewlet .debug-breakpoints .breakpoint > .file-path { +.debug-pane .debug-breakpoints .breakpoint > .file-path { opacity: 0.7; font-size: 0.9em; margin-left: 0.8em; @@ -383,7 +383,7 @@ overflow: hidden; } -.debug-viewlet .debug-breakpoints .breakpoint .name { +.debug-pane .debug-breakpoints .breakpoint .name { overflow: hidden; text-overflow: ellipsis } diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index f751bebda62..da729176ce9 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 923462eee94..f561b8cc42b 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 4184deb5dcd..4dd9c4bfac0 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -90,6 +90,7 @@ export class VariablesView extends ViewPane { renderBody(container: HTMLElement): void { super.renderBody(container); + dom.addClass(this.element, 'debug-pane'); dom.addClass(container, 'debug-variables'); const treeContainer = renderViewTree(container); @@ -109,10 +110,6 @@ export class VariablesView extends ViewPane { CONTEXT_VARIABLES_FOCUSED.bindTo(this.tree.contextKeyService); - if (this.toolbar) { - const collapseAction = new CollapseAction(this.tree, true, 'explorer-action codicon-collapse-all'); - this.toolbar.setActions([collapseAction])(); - } this.tree.updateChildren(); this._register(this.debugService.getViewModel().onDidFocusStackFrame(sf => { @@ -147,11 +144,10 @@ export class VariablesView extends ViewPane { this.tree.rerender(e); } })); - this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { - if (views.some(v => v.id === this.id)) { - this.tree.updateOptions({ overrideStyles: { listBackground: this.getBackgroundColor() } }); - } - })); + } + + getActions(): IAction[] { + return [new CollapseAction(this.tree, true, 'explorer-action codicon-collapse-all')]; } layoutBody(width: number, height: number): void { diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index f95c6728825..99b9cc6d32e 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -67,6 +67,7 @@ export class WatchExpressionsView extends ViewPane { renderBody(container: HTMLElement): void { super.renderBody(container); + dom.addClass(this.element, 'debug-pane'); dom.addClass(container, 'debug-watch'); const treeContainer = renderViewTree(container); @@ -95,13 +96,6 @@ export class WatchExpressionsView extends ViewPane { this.tree.setInput(this.debugService); CONTEXT_WATCH_EXPRESSIONS_FOCUSED.bindTo(this.tree.contextKeyService); - if (this.toolbar) { - const addWatchExpressionAction = new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService); - const collapseAction = new CollapseAction(this.tree, true, 'explorer-action codicon-collapse-all'); - const removeAllWatchExpressionsAction = new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService); - this.toolbar.setActions([addWatchExpressionAction, collapseAction, removeAllWatchExpressionsAction])(); - } - this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e))); this._register(this.debugService.getModel().onDidChangeWatchExpressions(async we => { @@ -145,11 +139,6 @@ export class WatchExpressionsView extends ViewPane { this.tree.rerender(e); } })); - this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { - if (views.some(v => v.id === this.id)) { - this.tree.updateOptions({ overrideStyles: { listBackground: this.getBackgroundColor() } }); - } - })); } layoutBody(height: number, width: number): void { @@ -160,6 +149,14 @@ export class WatchExpressionsView extends ViewPane { this.tree.domFocus(); } + getActions(): IAction[] { + return [ + new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService), + new CollapseAction(this.tree, true, 'explorer-action codicon-collapse-all'), + new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService) + ]; + } + private onMouseDblClick(e: ITreeMouseEvent): void { if ((e.browserEvent.target as HTMLElement).className.indexOf('twistie') >= 0) { // Ignore double click events on twistie diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 6d540653dbf..1df7822de47 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -200,9 +200,6 @@ export interface IDebugSession extends ITreeElement { readonly onDidLoadedSource: Event; readonly onDidCustomEvent: Event; - // Disconnects and clears state. Session can be initialized again for a new connection. - shutdown(): void; - // DAP request initialize(dbgr: IDebugger): Promise; @@ -467,7 +464,7 @@ export interface IDebugConfiguration { closeOnEnd: boolean; }; focusWindowOnBreak: boolean; - onTaskErrors: 'debugAnyway' | 'showErrors' | 'prompt' | 'cancel'; + onTaskErrors: 'debugAnyway' | 'showErrors' | 'prompt' | 'abort'; showBreakpointsInOverviewRuler: boolean; showInlineBreakpointCandidates: boolean; } diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 67de4eb4bee..6995887eefe 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -6,7 +6,6 @@ import * as nls from 'vs/nls'; import { URI as uri } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; -import * as lifecycle from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { generateUuid } from 'vs/base/common/uuid'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -819,7 +818,6 @@ export class ThreadAndSessionIds implements ITreeElement { export class DebugModel implements IDebugModel { private sessions: IDebugSession[]; - private toDispose: lifecycle.IDisposable[]; private schedulers = new Map(); private breakpointsActivated = true; private readonly _onDidChangeBreakpoints = new Emitter(); @@ -835,7 +833,6 @@ export class DebugModel implements IDebugModel { private textFileService: ITextFileService ) { this.sessions = []; - this.toDispose = []; } getId(): string { @@ -1227,10 +1224,4 @@ export class DebugModel implements IDebugModel { }); this._onDidChangeCallStack.fire(undefined); } - - dispose(): void { - // Make sure to shutdown each session, such that no debugged process is left laying around - this.sessions.forEach(s => s.shutdown()); - this.toDispose = lifecycle.dispose(this.toDispose); - } } diff --git a/src/vs/workbench/contrib/debug/common/debugSource.ts b/src/vs/workbench/contrib/debug/common/debugSource.ts index 77740aa032f..ca090ad6399 100644 --- a/src/vs/workbench/contrib/debug/common/debugSource.ts +++ b/src/vs/workbench/contrib/debug/common/debugSource.ts @@ -73,7 +73,7 @@ export class Source { openInEditor(editorService: IEditorService, selection: IRange, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise { return !this.available ? Promise.resolve(undefined) : editorService.openEditor({ - resource: this.uri.with({ query: null }), + resource: this.uri, description: this.origin, options: { preserveFocus, diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index a14e52c7a18..6be7695e38f 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -94,7 +94,8 @@ export function prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments } let quote: (s: string) => string; - let command = ''; + // begin command with a space to avoid polluting shell history + let command = ' '; switch (shellType) { diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index 31844187fbe..9fd5034f633 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -19,7 +19,7 @@ import { OverviewRulerLane } from 'vs/editor/common/model'; import { MarkdownString } from 'vs/base/common/htmlContent'; function createMockSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession { - return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!); + return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!); } function addBreakpointsAndCheckEvents(model: DebugModel, uri: uri, data: IBreakpointData[]): void { diff --git a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts index 558aa88393d..84b31372c66 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -18,7 +18,7 @@ import { getContext, getContextForContributedActions } from 'vs/workbench/contri import { getStackFrameThreadAndSessionToFocus } from 'vs/workbench/contrib/debug/browser/debugService'; export function createMockSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession { - return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!); + return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!); } function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFrame, secondStackFrame: StackFrame } { @@ -363,7 +363,7 @@ suite('Debug - CallStack', () => { get state(): State { return State.Stopped; } - }({ resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!); + }({ resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!); const runningSession = createMockSession(model); model.addSession(runningSession); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 14d4a539a74..13f7b1a7e38 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -327,8 +327,6 @@ export class MockSession implements IDebugSession { goto(threadId: number, targetId: number): Promise { throw new Error('Method not implemented.'); } - - shutdown(): void { } } export class MockRawSession { diff --git a/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts index 6ba96158d52..c7eba121141 100644 --- a/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts @@ -30,7 +30,7 @@ suite('Debug - ANSI Handling', () => { */ setup(() => { model = new DebugModel([], [], [], [], [], { isDirty: (e: any) => false }); - session = new DebugSession({ resolved: { name: 'test', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!); + session = new DebugSession({ resolved: { name: 'test', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!); const instantiationService: TestInstantiationService = workbenchInstantiationService(); linkDetector = instantiationService.createInstance(LinkDetector); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 3228cb72d0c..49ffa58bb98 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 5c214b0641d..af798c52dd8 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/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index cc45efefec9..2850d3ea76b 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -108,6 +108,7 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor order: 0, when: OpenEditorsVisibleContext, canToggleVisibility: true, + canMoveView: true, focusCommand: { id: 'workbench.files.action.focusOpenEditorsView', keybindings: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_E) } diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index 8f25376b925..ce694ddae30 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -19,7 +19,6 @@ } .explorer-viewlet .explorer-item, -.explorer-viewlet .open-editor, .explorer-viewlet .editor-group { height: 22px; line-height: 22px; @@ -31,7 +30,6 @@ } .explorer-viewlet .explorer-item > a, -.explorer-viewlet .open-editor > a, .explorer-viewlet .editor-group { text-overflow: ellipsis; overflow: hidden; @@ -50,16 +48,6 @@ flex: 0; /* do not steal space when label is hidden because we are in edit mode */ } -.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row { - padding-left: 22px; - display: flex; -} - -.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar { - visibility: hidden; - display: flex; - align-items: center; -} .explorer-viewlet .pane-header .count { min-width: fit-content; @@ -72,42 +60,6 @@ display: none; } -.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row:hover > .monaco-action-bar, -.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.focused > .monaco-action-bar, -.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.dirty > .monaco-action-bar { - visibility: visible; -} - -.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar .action-label { - display: block; -} - -.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar .codicon { - color: inherit; -} - -.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar .codicon-close { - width: 8px; - height: 22px; - display: flex; - align-items: center; - justify-content: center; -} - -.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar .action-close-all-files, -.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar .save-all { - width: 23px; - height: 22px; -} - -.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .open-editor { - flex: 1; -} - -.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .editor-group { - flex: 1; -} - .explorer-viewlet .monaco-count-badge { padding: 1px 6px 2px; margin-left: 6px; @@ -155,24 +107,7 @@ height: 20px; } -.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row .editor-group { - font-size: 11px; - font-weight: bold; - text-transform: uppercase; - cursor: default; -} - -/* Bold font style does not go well with CJK fonts */ -.explorer-viewlet:lang(zh-Hans) .explorer-open-editors .monaco-list .monaco-list-row .editor-group, -.explorer-viewlet:lang(zh-Hant) .explorer-open-editors .monaco-list .monaco-list-row .editor-group, -.explorer-viewlet:lang(ja) .explorer-open-editors .monaco-list .monaco-list-row .editor-group, -.explorer-viewlet:lang(ko) .explorer-open-editors .monaco-list .monaco-list-row .editor-group { - font-weight: normal; -} - /* High Contrast Theming */ -.hc-black .monaco-workbench .explorer-viewlet .explorer-item, -.hc-black .monaco-workbench .explorer-viewlet .open-editor, -.hc-black .monaco-workbench .explorer-viewlet .editor-group { +.hc-black .monaco-workbench .explorer-viewlet .explorer-item { line-height: 20px; } diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index 66fd1ce815b..c4f45d6d313 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -50,7 +50,7 @@ export class EmptyView extends ViewPane { @IContextKeyService contextKeyService: IContextKeyService, @IOpenerService openerService: IOpenerService ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('explorerSection', "Explorer Section: No Folder Opened") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this._register(this.contextService.onDidChangeWorkbenchState(() => this.setLabels())); this._register(this.labelService.onDidChangeFormatters(() => this.setLabels())); } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 8cd40c0c99b..1af95f4a23c 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -172,7 +172,7 @@ export class ExplorerView extends ViewPane { @IFileService private readonly fileService: IFileService, @IOpenerService openerService: IOpenerService, ) { - super({ ...(options as IViewPaneOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Explorer Section: {0}", labelService.getWorkspaceLabel(contextService.getWorkspace())) }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); this.resourceContext = instantiationService.createInstance(ResourceContextKey); this._register(this.resourceContext); @@ -229,6 +229,7 @@ export class ExplorerView extends ViewPane { const title = workspace.folders.map(folder => folder.name).join(); titleElement.textContent = this.name; titleElement.title = title; + titleElement.setAttribute('aria-label', nls.localize('explorerSection', "Explorer Section: {0}", this.name)); }; this._register(this.contextService.onDidChangeWorkspaceName(setHeader)); @@ -250,10 +251,6 @@ export class ExplorerView extends ViewPane { this.createTree(treeContainer); - if (this.toolbar) { - this.toolbar.setActions(this.getActions(), this.getSecondaryActions())(); - } - this._register(this.labelService.onDidChangeFormatters(() => { this._onDidChangeTitleArea.fire(); })); @@ -679,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/files/browser/views/media/openeditors.css b/src/vs/workbench/contrib/files/browser/views/media/openeditors.css new file mode 100644 index 00000000000..a64e503df77 --- /dev/null +++ b/src/vs/workbench/contrib/files/browser/views/media/openeditors.css @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.open-editors .monaco-list .monaco-list-row:hover > .monaco-action-bar, +.open-editors .monaco-list .monaco-list-row.focused > .monaco-action-bar, +.open-editors .monaco-list .monaco-list-row.dirty > .monaco-action-bar { + visibility: visible; +} + +.open-editors .monaco-list .monaco-list-row > .monaco-action-bar .action-label { + display: block; +} + +.open-editors .monaco-list .monaco-list-row > .monaco-action-bar .codicon { + color: inherit; +} + +.open-editors .monaco-list .monaco-list-row > .monaco-action-bar .codicon-close { + width: 8px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; +} + +.open-editors .monaco-list .monaco-list-row > .monaco-action-bar .action-close-all-files, +.open-editors .monaco-list .monaco-list-row > .monaco-action-bar .save-all { + width: 23px; + height: 22px; +} + +.open-editors .monaco-list .monaco-list-row > .open-editor { + flex: 1; +} + +.open-editors .monaco-list .monaco-list-row > .editor-group { + flex: 1; +} + +.open-editors .monaco-list .monaco-list-row { + padding-left: 22px; + display: flex; +} + +.open-editors .monaco-list .monaco-list-row > .monaco-action-bar { + visibility: hidden; + display: flex; + align-items: center; +} + +.open-editors .monaco-list .monaco-list-row .editor-group { + font-size: 11px; + font-weight: bold; + text-transform: uppercase; + cursor: default; +} + +/* Bold font style does not go well with CJK fonts */ +.composite:lang(zh-Hans) .open-editors .monaco-list .monaco-list-row .editor-group, +.composite:lang(zh-Hant) .open-editors .monaco-list .monaco-list-row .editor-group, +.composite:lang(ja) .open-editors .monaco-list .monaco-list-row .editor-group, +.composite:lang(ko) .open-editors .monaco-list .monaco-list-row .editor-group { + font-weight: normal; +} + +.open-editors .open-editor, +.open-editors .editor-group { + height: 22px; + line-height: 22px; +} + +.open-editors .open-editor > a, +.open-editors .editor-group { + text-overflow: ellipsis; + overflow: hidden; +} + +.hc-black .monaco-workbench .open-editors .open-editor, +.hc-black .monaco-workbench .open-editors .editor-group { + line-height: 20px; +} diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index c4f4a7ff572..7e53ad1f395 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/openeditors'; import * as nls from 'vs/nls'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IAction, ActionRunner, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; @@ -42,7 +43,6 @@ import { URI } from 'vs/base/common/uri'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { isWeb } from 'vs/base/common/platform'; import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -206,7 +206,7 @@ export class OpenEditorsView extends ViewPane { renderBody(container: HTMLElement): void { super.renderBody(container); - dom.addClass(container, 'explorer-open-editors'); + dom.addClass(container, 'open-editors'); dom.addClass(container, 'show-file-icons'); const delegate = new OpenEditorsDelegate(); @@ -225,7 +225,7 @@ export class OpenEditorsView extends ViewPane { identityProvider: { getId: (element: OpenEditor | IEditorGroup) => element instanceof OpenEditor ? element.getId() : element.id.toString() }, dnd: new OpenEditorsDragAndDrop(this.instantiationService, this.editorGroupService), overrideStyles: { - listBackground: SIDE_BAR_BACKGROUND + listBackground: this.getBackgroundColor() } }); this._register(this.list); diff --git a/src/vs/workbench/contrib/logs/electron-browser/logs.contribution.ts b/src/vs/workbench/contrib/logs/electron-browser/logs.contribution.ts index 5aa0badb11e..f98f5653b1b 100644 --- a/src/vs/workbench/contrib/logs/electron-browser/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/electron-browser/logs.contribution.ts @@ -7,8 +7,9 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { OpenLogsFolderAction } from 'vs/workbench/contrib/logs/electron-browser/logsActions'; +import { OpenLogsFolderAction, OpenExtensionLogsFolderAction } from 'vs/workbench/contrib/logs/electron-browser/logsActions'; const workbenchActionsRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); const devCategory = nls.localize('developer', "Developer"); workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.create(OpenLogsFolderAction, OpenLogsFolderAction.ID, OpenLogsFolderAction.LABEL), 'Developer: Open Logs Folder', devCategory); +workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.create(OpenExtensionLogsFolderAction, OpenExtensionLogsFolderAction.ID, OpenExtensionLogsFolderAction.LABEL), 'Developer: Open Extension Logs Folder', devCategory); diff --git a/src/vs/workbench/contrib/logs/electron-browser/logsActions.ts b/src/vs/workbench/contrib/logs/electron-browser/logsActions.ts index f2287b85265..9834a34dc62 100644 --- a/src/vs/workbench/contrib/logs/electron-browser/logsActions.ts +++ b/src/vs/workbench/contrib/logs/electron-browser/logsActions.ts @@ -3,12 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { join } from 'vs/base/common/path'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { URI } from 'vs/base/common/uri'; +import * as nls from 'vs/nls'; import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IElectronEnvironmentService } from 'vs/workbench/services/electron/electron-browser/electronEnvironmentService'; +import { IFileService } from 'vs/platform/files/common/files'; export class OpenLogsFolderAction extends Action { @@ -26,3 +28,24 @@ export class OpenLogsFolderAction extends Action { return this.electronService.showItemInFolder(URI.file(join(this.environmentService.logsPath, 'main.log')).fsPath); } } + +export class OpenExtensionLogsFolderAction extends Action { + + static readonly ID = 'workbench.action.openExtensionLogsFolder'; + static readonly LABEL = nls.localize('openExtensionLogsFolder', "Open Extension Logs Folder"); + + constructor(id: string, label: string, + @IElectronEnvironmentService private readonly electronEnvironmentSerice: IElectronEnvironmentService, + @IFileService private readonly fileService: IFileService, + @IElectronService private readonly electronService: IElectronService + ) { + super(id, label); + } + + async run(): Promise { + const folderStat = await this.fileService.resolve(this.electronEnvironmentSerice.extHostLogsPath); + if (folderStat.children && folderStat.children[0]) { + return this.electronService.showItemInFolder(folderStat.children[0].resource.fsPath); + } + } +} 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 2dc49482c8d..9854d0f0628 100644 --- a/src/vs/workbench/contrib/openInDesktop/browser/openInDesktop.web.contribution.ts +++ b/src/vs/workbench/contrib/openInDesktop/browser/openInDesktop.web.contribution.ts @@ -18,6 +18,7 @@ import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IApplicationLink } from 'vs/workbench/workbench.web.api'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export class OpenInDesktopIndicator extends Disposable implements IWorkbenchContribution { @@ -28,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); @@ -63,16 +64,17 @@ export class OpenInDesktopAction extends Action { id: string, label: string, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IOpenerService private readonly openerService: IOpenerService ) { super(id, label); } 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.runWithoutPicker(links[0]); + return this.openApplicationLink(links[0]); } return this.runWithPicker(links); @@ -81,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(); @@ -91,7 +93,7 @@ export class OpenInDesktopAction extends Action { quickPick.onDidAccept(() => { const selectedItems = quickPick.selectedItems; if (selectedItems.length === 1) { - this.runWithoutPicker(selectedItems[0]); + this.openApplicationLink(selectedItems[0]); } quickPick.hide(); }); @@ -101,10 +103,8 @@ export class OpenInDesktopAction extends Action { return true; } - private async runWithoutPicker(link: IApplicationLink): Promise { - - // Open directly - window.location.href = link.uri.toString(); + private async openApplicationLink(link: IApplicationLink): Promise { + this.openerService.open(link.uri, { openExternal: true }); return true; } diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 7214dddd208..c4ba64d1e82 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 60bcbfd4ba4..a8865d6ec60 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 a152b50423d..8ff6aff2343 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/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 7debeeb851e..dbd34cdc3d8 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -18,6 +18,8 @@ import { joinPath } from 'vs/base/common/resources'; import { Disposable } from 'vs/base/common/lifecycle'; import { TunnelFactoryContribution } from 'vs/workbench/contrib/remote/common/tunnelFactory'; import { ShowCandidateContribution } from 'vs/workbench/contrib/remote/common/showCandidate'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; export const VIEWLET_ID = 'workbench.view.remote'; @@ -90,3 +92,37 @@ workbenchContributionsRegistry.registerWorkbenchContribution(RemoteChannelsContr workbenchContributionsRegistry.registerWorkbenchContribution(RemoteLogOutputChannels, LifecyclePhase.Restored); workbenchContributionsRegistry.registerWorkbenchContribution(TunnelFactoryContribution, LifecyclePhase.Ready); workbenchContributionsRegistry.registerWorkbenchContribution(ShowCandidateContribution, LifecyclePhase.Ready); + +const extensionKindSchema: IJSONSchema = { + type: 'string', + enum: [ + 'ui', + 'workspace' + ], + enumDescriptions: [ + localize('ui', "UI extension kind. In a remote window, such extensions are enabled only when available on the local machine."), + localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote.") + ], +}; + +Registry.as(ConfigurationExtensions.Configuration) + .registerConfiguration({ + id: 'remote', + title: localize('remote', "Remote"), + type: 'object', + properties: { + 'remote.extensionKind': { + type: 'object', + markdownDescription: localize('remote.extensionKind', "Override the kind of an extension. `ui` extensions are installed and run on the local machine while `workspace` extensions are run on the remote. By overriding an extension's default kind using this setting, you specify if that extension should be installed and enabled locally or remotely."), + patternProperties: { + '([a-z0-9A-Z][a-z0-9\-A-Z]*)\\.([a-z0-9A-Z][a-z0-9\-A-Z]*)$': { + oneOf: [{ type: 'array', items: extensionKindSchema }, extensionKindSchema], + default: ['ui'], + }, + }, + default: { + 'pub.name': ['ui'] + } + }, + } + }); diff --git a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts index 755d1d14ebc..eb03c3f76d7 100644 --- a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts @@ -38,7 +38,6 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { RemoteConnectionState, Deprecated_RemoteAuthorityContext, RemoteFileDialogContext } from 'vs/workbench/browser/contextkeys'; import { IDownloadService } from 'vs/platform/download/common/download'; import { OpenLocalFileFolderCommand, OpenLocalFileCommand, OpenLocalFolderCommand, SaveLocalFileCommand } from 'vs/workbench/services/dialogs/browser/simpleFileDialog'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; const WINDOW_ACTIONS_COMMAND_ID = 'workbench.action.remote.showMenu'; const CLOSE_REMOTE_COMMAND_ID = 'workbench.action.remote.close'; @@ -370,37 +369,12 @@ workbenchContributionsRegistry.registerWorkbenchContribution(RemoteWindowActiveI workbenchContributionsRegistry.registerWorkbenchContribution(RemoteTelemetryEnablementUpdater, LifecyclePhase.Ready); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteEmptyWorkbenchPresentation, LifecyclePhase.Starting); -const extensionKindSchema: IJSONSchema = { - type: 'string', - enum: [ - 'ui', - 'workspace' - ], - enumDescriptions: [ - nls.localize('ui', "UI extension kind. In a remote window, such extensions are enabled only when available on the local machine."), - nls.localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote.") - ], -}; - Registry.as(ConfigurationExtensions.Configuration) .registerConfiguration({ id: 'remote', title: nls.localize('remote', "Remote"), type: 'object', properties: { - 'remote.extensionKind': { - type: 'object', - markdownDescription: nls.localize('remote.extensionKind', "Override the kind of an extension. `ui` extensions are installed and run on the local machine while `workspace` extensions are run on the remote. By overriding an extension's default kind using this setting, you specify if that extension should be installed and enabled locally or remotely."), - patternProperties: { - '([a-z0-9A-Z][a-z0-9\-A-Z]*)\\.([a-z0-9A-Z][a-z0-9\-A-Z]*)$': { - oneOf: [{ type: 'array', items: extensionKindSchema }, extensionKindSchema], - default: ['ui'], - }, - }, - default: { - 'pub.name': ['ui'] - } - }, 'remote.downloadExtensionsLocally': { type: 'boolean', markdownDescription: nls.localize('remote.downloadExtensionsLocally', "When enabled extensions are downloaded locally and installed on remote."), diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index 482b66fb18d..9c94a5c49eb 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 3f65b434ea8..ab9c041d125 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 128c0029152..035f4617e54 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/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index 6de22f21f68..3f8d572d78a 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -448,8 +448,9 @@ export class SearchWidget extends Widget { } setValue(value: string, skipSearchOnChange: boolean) { - this.temporarilySkipSearchOnChange = skipSearchOnChange || this.temporarilySkipSearchOnChange; + this.temporarilySkipSearchOnChange = skipSearchOnChange; this.searchInput.setValue(value); + this.temporarilySkipSearchOnChange = false; } setReplaceAllActionState(enabled: boolean): void { @@ -491,9 +492,7 @@ export class SearchWidget extends Widget { this.setReplaceAllActionState(false); if (this.searchConfiguration.searchOnType) { - if (this.temporarilySkipSearchOnChange) { - this.temporarilySkipSearchOnChange = false; - } else { + if (!this.temporarilySkipSearchOnChange) { this._onSearchCancel.fire({ focus: false }); if (this.searchInput.getRegex()) { try { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index 653cf67456b..1884a02ec44 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 d348146e3dc..e07b696c2cf 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -112,30 +112,12 @@ 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); const telemetryService = accessor.get(ITelemetryService); const instantiationService = accessor.get(IInstantiationService); + const configurationService = accessor.get(IConfigurationService); const activeEditor = editorService.activeTextEditorWidget; let activeModel: ICodeEditor | undefined; @@ -162,7 +144,11 @@ const openNewSearchEditor = telemetryService.publicLog2('searchEditor/openNewSearchEditor'); const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: { query: selected } }); - await editorService.openEditor(input, { pinned: true }); + const editor = await editorService.openEditor(input, { pinned: true }) as SearchEditor; + + if (selected && configurationService.getValue('search').searchOnType) { + editor.runSearch(true, true); + } }; export const createEditorFromSearchResult = diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index c6f0946e123..9ad11415ffe 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -332,11 +332,6 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: true }, - 'terminal.integrated.experimentalRefreshOnResume': { - description: nls.localize('terminal.integrated.experimentalRefreshOnResume', "An experimental setting that will refresh the terminal renderer when the system is resumed."), - type: 'boolean', - default: false - }, 'terminal.integrated.experimentalUseTitleEvent': { description: nls.localize('terminal.integrated.experimentalUseTitleEvent', "An experimental setting that will use the terminal title event for the dropdown title. This setting will only apply to new terminals."), type: 'boolean', diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index fd41e75e93c..cec072d72b3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -847,15 +847,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (!this._xterm) { return; } - if (this._configHelper.config.experimentalRefreshOnResume) { - if (this._xterm.getOption('rendererType') !== 'dom') { - this._xterm.setOption('rendererType', 'dom'); - // Do this asynchronously to clear our the texture atlas as all terminals will not - // be using canvas - const xterm = this._xterm; - setTimeout(() => xterm.setOption('rendererType', 'canvas'), 0); - } - } this._xterm.refresh(0, this._xterm.rows - 1); } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index a0a28c39e8f..e58b63cc000 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -119,7 +119,6 @@ export interface ITerminalConfiguration { showExitAlert: boolean; splitCwd: 'workspaceRoot' | 'initial' | 'inherited'; windowsEnableConpty: boolean; - experimentalRefreshOnResume: boolean; experimentalUseTitleEvent: boolean; enableFileLinks: boolean; unicodeVersion: '6' | '11'; diff --git a/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css b/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css index ae431e80709..ad6200c6561 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/timeline.contribution.ts b/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts index ab922d819ae..142fb5cb596 100644 --- a/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts +++ b/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts @@ -26,6 +26,8 @@ export class TimelinePaneDescriptor implements IViewDescriptor { readonly collapsed = true; readonly canToggleVisibility = true; readonly hideByDefault = false; + readonly canMoveView = true; + focusCommand = { id: 'timeline.focus' }; } diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 393d822695f..1afbedc6563 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,14 +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() + keyboardNavigationLabelProvider: new TimelineKeyboardNavigationLabelProvider(), + overrideStyles: { + 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) { @@ -313,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; } } @@ -350,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( @@ -366,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 65fe341ffc0..7ffca608646 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 eda52441831..27038106272 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 91dc0c6fcaf..786014463b6 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 5e756cd3704..6af68ea3feb 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 5cf9df18ef8..d8f0b88ebc3 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 9848ce0cb70..fe444e8070d 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 63c9af47e20..aa318da9d65 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 c1ce99713e4..b8240e1bc17 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 784bef65db4..b65f28416f2 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 9845fb414d9..5f870d1d15e 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 a0e741c2917..97eb2c50bd0 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 538e36abc33..b0471a3d0a1 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 611622ed72b..ebbe40c7f42 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 00000000000..9153e3c1a4d --- /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 00000000000..51fb3194e95 --- /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 00000000000..28a5bc02dfa --- /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 61e2e2e5885..f1d8f9af253 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/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index 9b4db895695..e9da83b049d 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -182,7 +182,8 @@ class DesktopMain extends Disposable { serviceCollection.set(IWorkbenchEnvironmentService, this.environmentService); serviceCollection.set(IElectronEnvironmentService, new ElectronEnvironmentService( this.configuration.windowId, - this.environmentService.sharedIPCHandle + this.environmentService.sharedIPCHandle, + this.environmentService )); // Product diff --git a/src/vs/workbench/services/accessibility/node/accessibilityService.ts b/src/vs/workbench/services/accessibility/node/accessibilityService.ts index faedc3fd318..a2911ba04a5 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/electron/electron-browser/electronEnvironmentService.ts b/src/vs/workbench/services/electron/electron-browser/electronEnvironmentService.ts index ead4c15e1ac..9902c63e3ec 100644 --- a/src/vs/workbench/services/electron/electron-browser/electronEnvironmentService.ts +++ b/src/vs/workbench/services/electron/electron-browser/electronEnvironmentService.ts @@ -4,6 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { URI } from 'vs/base/common/uri'; +import { memoize } from 'vs/base/common/decorators'; +import { join } from 'vs/base/common/path'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; export const IElectronEnvironmentService = createDecorator('electronEnvironmentService'); @@ -14,6 +18,8 @@ export interface IElectronEnvironmentService { readonly windowId: number; readonly sharedIPCHandle: string; + + readonly extHostLogsPath: URI; } export class ElectronEnvironmentService implements IElectronEnvironmentService { @@ -22,6 +28,10 @@ export class ElectronEnvironmentService implements IElectronEnvironmentService { constructor( public readonly windowId: number, - public readonly sharedIPCHandle: string + public readonly sharedIPCHandle: string, + private readonly environmentService: IEnvironmentService ) { } + + @memoize + get extHostLogsPath(): URI { return URI.file(join(this.environmentService.logsPath, `exthost${this.windowId}`)); } } diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index 41423b188fa..01d63f94a76 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/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 8e2bf659aeb..a3a45ed8ec4 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -9,9 +9,7 @@ import { CachedExtensionScanner } from 'vs/workbench/services/extensions/electro import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { AbstractExtensionService } from 'vs/workbench/services/extensions/common/abstractExtensionService'; import * as nls from 'vs/nls'; -import * as path from 'vs/base/common/path'; import { runWhenIdle } from 'vs/base/common/async'; -import { URI } from 'vs/base/common/uri'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -55,7 +53,6 @@ export class ExtensionService extends AbstractExtensionService implements IExten private readonly _remoteExtensionsEnvironmentData: Map; - private readonly _extensionHostLogsLocation: URI; private readonly _extensionScanner: CachedExtensionScanner; private _deltaExtensionsQueue: DeltaExtensionsQueueItem[]; @@ -99,7 +96,6 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteExtensionsEnvironmentData = new Map(); - this._extensionHostLogsLocation = URI.file(path.join(this._environmentService.logsPath, `exthost${this._electronEnvironmentService.windowId}`)); this._extensionScanner = instantiationService.createInstance(CachedExtensionScanner); this._deltaExtensionsQueue = []; @@ -363,7 +359,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten const result: ExtensionHostProcessManager[] = []; - const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, autoStart, extensions, this._extensionHostLogsLocation); + const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, autoStart, extensions, this._electronEnvironmentService.extHostLogsPath); const extHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, true, extHostProcessWorker, null, initialActivationEvents); result.push(extHostProcessManager); diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 7c0b27ba490..85de4203e54 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 f97c8048de2..ad10de5ba65 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 6fd4fa2404e..b009d228026 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 6b73a2f3a4e..a1e92a76439 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 4a5831b5667..4ffbfbbd4c8 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 1f6c9878fa5..94a61318684 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/saveSequenzializer.ts b/src/vs/workbench/services/textfile/common/saveSequenzializer.ts deleted file mode 100644 index 2b7cc0c8282..00000000000 --- a/src/vs/workbench/services/textfile/common/saveSequenzializer.ts +++ /dev/null @@ -1,95 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -interface IPendingSave { - versionId: number; - promise: Promise; -} - -interface ISaveOperation { - promise: Promise; - promiseResolve: () => void; - promiseReject: (error: Error) => void; - run: () => Promise; -} - -export class SaveSequentializer { - private _pendingSave?: IPendingSave; - private _nextSave?: ISaveOperation; - - hasPendingSave(versionId?: number): boolean { - if (!this._pendingSave) { - return false; - } - - if (typeof versionId === 'number') { - return this._pendingSave.versionId === versionId; - } - - return !!this._pendingSave; - } - - get pendingSave(): Promise | undefined { - return this._pendingSave ? this._pendingSave.promise : undefined; - } - - setPending(versionId: number, promise: Promise): Promise { - this._pendingSave = { versionId, promise }; - - promise.then(() => this.donePending(versionId), () => this.donePending(versionId)); - - return promise; - } - - private donePending(versionId: number): void { - if (this._pendingSave && versionId === this._pendingSave.versionId) { - - // only set pending to done if the promise finished that is associated with that versionId - this._pendingSave = undefined; - - // schedule the next save now that we are free if we have any - this.triggerNextSave(); - } - } - - private triggerNextSave(): void { - if (this._nextSave) { - const saveOperation = this._nextSave; - this._nextSave = undefined; - - // Run next save and complete on the associated promise - saveOperation.run().then(saveOperation.promiseResolve, saveOperation.promiseReject); - } - } - - setNext(run: () => Promise): Promise { - - // this is our first next save, so we create associated promise with it - // so that we can return a promise that completes when the save operation - // has completed. - if (!this._nextSave) { - let promiseResolve: () => void; - let promiseReject: (error: Error) => void; - const promise = new Promise((resolve, reject) => { - promiseResolve = resolve; - promiseReject = reject; - }); - - this._nextSave = { - run, - promise, - promiseResolve: promiseResolve!, - promiseReject: promiseReject! - }; - } - - // we have a previous next save, just overwrite it - else { - this._nextSave.run = run; - } - - return this._nextSave.promise; - } -} diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 52ef8b8e949..5b622561b3a 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -14,7 +14,7 @@ import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backu import { IFileService, FileOperationError, FileOperationResult, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { timeout } from 'vs/base/common/async'; +import { timeout, TaskSequentializer } from 'vs/base/common/async'; import { ITextBufferFactory, ITextModel } from 'vs/editor/common/model'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ILogService } from 'vs/platform/log/common/log'; @@ -22,8 +22,8 @@ import { basename } from 'vs/base/common/path'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IWorkingCopyService, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { SaveSequentializer } from 'vs/workbench/services/textfile/common/saveSequenzializer'; import { ILabelService } from 'vs/platform/label/common/label'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; interface IBackupMetaData { mtime: number; @@ -78,8 +78,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private lastResolvedFileStat: IFileStatWithMetadata | undefined; - private readonly saveSequentializer = new SaveSequentializer(); - private lastSaveAttemptTime = 0; + private readonly saveSequentializer = new TaskSequentializer(); private dirty = false; private inConflictMode = false; @@ -255,7 +254,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // It is very important to not reload the model when the model is dirty. // We also only want to reload the model from the disk if no save is pending // to avoid data loss. - if (this.dirty || this.saveSequentializer.hasPendingSave()) { + if (this.dirty || this.saveSequentializer.hasPending()) { this.logService.trace('[text file model] load() - exit - without loading because model is dirty or being saved', this.resource.toString()); return this; @@ -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; } @@ -575,10 +573,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Scenario: user invoked the save action multiple times quickly for the same contents // while the save was not yet finished to disk // - if (this.saveSequentializer.hasPendingSave(versionId)) { + if (this.saveSequentializer.hasPending(versionId)) { this.logService.trace(`[text file model] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource.toString()); - return this.saveSequentializer.pendingSave || Promise.resolve(); + return this.saveSequentializer.pending; } // Return early if not dirty (unless forced) @@ -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. @@ -598,9 +596,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Scenario B: save is very slow (e.g. network share) and the user manages to change the buffer and trigger another save // while the first save has not returned yet. // - if (this.saveSequentializer.hasPendingSave()) { + if (this.saveSequentializer.hasPending()) { this.logService.trace(`[text file model] doSave(${versionId}) - exit - because busy saving`, this.resource.toString()); + // Indicate to the save sequentializer that we want to + // cancel the pending operation so that ours can run + // before the pending one finishes. + // Currently this will try to cancel pending save + // participants but never a pending save. + this.saveSequentializer.cancelPending(); + // Register this as the next upcoming save and return return this.saveSequentializer.setNext(() => this.doSave(options)); } @@ -611,25 +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. - 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 }).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 @@ -653,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))); - })); + 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 { @@ -717,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(); @@ -725,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 { @@ -768,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: @@ -783,7 +805,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil case ModelState.ORPHAN: return this.inOrphanMode; case ModelState.PENDING_SAVE: - return this.saveSequentializer.hasPendingSave(); + return this.saveSequentializer.hasPending(); case ModelState.SAVED: return !this.dirty; } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 337797b264b..a3a6cdba937 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 00000000000..2ba91174b67 --- /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 f211c5939fd..c9532133e00 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -16,6 +16,8 @@ import { isUndefinedOrNull } from 'vs/base/common/types'; 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'); @@ -50,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. * @@ -225,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, env: { reason: SaveReason }): Promise; -} - /** * States the text file editor model can be in. */ @@ -356,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; @@ -371,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 34ffc230de4..def5df7ccbe 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 d6bc866bf8f..af2880b2e16 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,55 +502,180 @@ 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 () { + const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + let participations: boolean[] = []; + + const participant = accessor.textFileService.files.addSaveParticipant({ + participate: async (model, context, progress, token) => { + await timeout(10); + + if (!token.isCancellationRequested) { + participations.push(true); + } + } + }); + + await model.load(); + + model.textEditorModel!.setValue('foo'); + const p1 = model.save(); + + model.textEditorModel!.setValue('foo 1'); + const p2 = model.save(); + + model.textEditorModel!.setValue('foo 2'); + const p3 = model.save(); + + 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/textfile/test/common/saveSequenzializer.test.ts b/src/vs/workbench/services/textfile/test/common/saveSequenzializer.test.ts deleted file mode 100644 index c85a38bd8b3..00000000000 --- a/src/vs/workbench/services/textfile/test/common/saveSequenzializer.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { timeout } from 'vs/base/common/async'; -import { SaveSequentializer } from 'vs/workbench/services/textfile/common/saveSequenzializer'; - -suite('Files - SaveSequentializer', () => { - - test('SaveSequentializer - pending basics', async function () { - const sequentializer = new SaveSequentializer(); - - assert.ok(!sequentializer.hasPendingSave()); - assert.ok(!sequentializer.hasPendingSave(2323)); - assert.ok(!sequentializer.pendingSave); - - // pending removes itself after done - await sequentializer.setPending(1, Promise.resolve()); - assert.ok(!sequentializer.hasPendingSave()); - assert.ok(!sequentializer.hasPendingSave(1)); - assert.ok(!sequentializer.pendingSave); - - // pending removes itself after done (use timeout) - sequentializer.setPending(2, timeout(1)); - assert.ok(sequentializer.hasPendingSave()); - assert.ok(sequentializer.hasPendingSave(2)); - assert.ok(!sequentializer.hasPendingSave(1)); - assert.ok(sequentializer.pendingSave); - - await timeout(2); - assert.ok(!sequentializer.hasPendingSave()); - assert.ok(!sequentializer.hasPendingSave(2)); - assert.ok(!sequentializer.pendingSave); - }); - - test('SaveSequentializer - pending and next (finishes instantly)', async function () { - const sequentializer = new SaveSequentializer(); - - let pendingDone = false; - sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return; })); - - // next finishes instantly - let nextDone = false; - const res = sequentializer.setNext(() => Promise.resolve(null).then(() => { nextDone = true; return; })); - - await res; - assert.ok(pendingDone); - assert.ok(nextDone); - }); - - test('SaveSequentializer - pending and next (finishes after timeout)', async function () { - const sequentializer = new SaveSequentializer(); - - let pendingDone = false; - sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return; })); - - // next finishes after timeout - let nextDone = false; - const res = sequentializer.setNext(() => timeout(1).then(() => { nextDone = true; return; })); - - await res; - assert.ok(pendingDone); - assert.ok(nextDone); - }); - - test('SaveSequentializer - pending and multiple next (last one wins)', async function () { - const sequentializer = new SaveSequentializer(); - - let pendingDone = false; - sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return; })); - - // next finishes after timeout - let firstDone = false; - let firstRes = sequentializer.setNext(() => timeout(2).then(() => { firstDone = true; return; })); - - let secondDone = false; - let secondRes = sequentializer.setNext(() => timeout(3).then(() => { secondDone = true; return; })); - - let thirdDone = false; - let thirdRes = sequentializer.setNext(() => timeout(4).then(() => { thirdDone = true; return; })); - - await Promise.all([firstRes, secondRes, thirdRes]); - assert.ok(pendingDone); - assert.ok(!firstDone); - assert.ok(!secondDone); - assert.ok(thirdDone); - }); -}); diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index e9592bbbbe2..05e918a797d 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -44,6 +44,11 @@ export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport */ readonly hasAssociatedFilePath: boolean; + /** + * Wether this model has an explicit language mode or not. + */ + readonly hasModeSetExplicitly: boolean; + /** * Sets the encoding to use for this untitled model. */ @@ -150,7 +155,14 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt return this.versionId; } + private _hasModeSetExplicitly: boolean = false; + get hasModeSetExplicitly(): boolean { return this._hasModeSetExplicitly; } + setMode(mode: string): void { + + // Remember that an explicit mode was set + this._hasModeSetExplicitly = true; + let actualMode: string | undefined = undefined; if (mode === '${activeEditorLanguage}') { // support the special '${activeEditorLanguage}' mode by diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index 62938a00d2d..011bbabbcd8 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -287,12 +287,34 @@ suite('Untitled text editors', () => { const service = accessor.untitledTextEditorService; const input = instantiationService.createInstance(UntitledTextEditorInput, service.create({ mode })); + assert.ok(input.model.hasModeSetExplicitly); assert.equal(input.getMode(), mode); const model = await input.resolve(); assert.equal(model.getMode(), mode); - input.setMode('text'); + input.setMode('plaintext'); + + assert.equal(input.getMode(), PLAINTEXT_MODE_ID); + + input.dispose(); + model.dispose(); + }); + + test('remembers that mode was set explicitly', async () => { + const mode = 'untitled-input-test'; + + ModesRegistry.registerLanguage({ + id: mode, + }); + + const service = accessor.untitledTextEditorService; + const model = service.create(); + const input = instantiationService.createInstance(UntitledTextEditorInput, model); + + assert.ok(!input.model.hasModeSetExplicitly); + input.setMode('plaintext'); + assert.ok(input.model.hasModeSetExplicitly); assert.equal(input.getMode(), PLAINTEXT_MODE_ID); diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index 14366966e16..230a76fe556 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 04ae1141412..50efaef1f8a 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 b8a5e1b1ee1..b95b7d7b1eb 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 0857cc45c11..db2309ddf7b 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 9debc489ee1..1e943601b66 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 8703e1a79fd..140711b300e 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -16,6 +16,7 @@ import { IUpdateProvider, IUpdate } from 'vs/workbench/services/update/browser/u import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IWorkspaceProvider, IWorkspace } from 'vs/workbench/services/host/browser/browserHostService'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; interface IResourceUriProvider { (uri: URI): URI; @@ -36,18 +37,23 @@ interface IExternalUriResolver { interface TunnelOptions { remoteAddress: { port: number, host: string }; - // The desired local port. If this port can't be used, then another will be chosen. + /** + * The desired local port. If this port can't be used, then another will be chosen. + */ localAddressPort?: number; label?: string; } -interface Tunnel { +interface Tunnel extends IDisposable { remoteAddress: { port: number, host: string }; - //The complete local address(ex. localhost:1234) + /** + * The complete local address(ex. localhost:1234) + */ localAddress: string; - // Implementers of Tunnel should fire onDidDispose when dispose is called. + /** + * Implementers of Tunnel should fire onDidDispose when dispose is called. + */ onDidDispose: Event; - dispose(): void; } interface ITunnelFactory { @@ -74,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. @@ -102,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. */ @@ -113,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. */ @@ -148,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. * @@ -173,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`. @@ -184,6 +220,8 @@ interface IWorkbenchConstructionOptions { * Whether to enable the smoke test driver. */ readonly driver?: boolean; + + //#endregion } /** @@ -192,8 +230,21 @@ interface IWorkbenchConstructionOptions { * @param domElement the container to create the workbench in * @param options for setting up the workbench */ -function create(domElement: HTMLElement, options: IWorkbenchConstructionOptions): Promise { - return main(domElement, options); +async function create(domElement: HTMLElement, options: IWorkbenchConstructionOptions): Promise { + + // Startup workbench + await main(domElement, options); + + // 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.handler(...args); + }); + } + } } export { @@ -202,6 +253,7 @@ export { create, IWorkbenchConstructionOptions, + // Basic Types URI, UriComponents, @@ -252,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 c18e01808d3..40ee4c82851 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/README.md b/test/integration/browser/README.md index 6140aab9acf..10a55f7de17 100644 --- a/test/integration/browser/README.md +++ b/test/integration/browser/README.md @@ -19,3 +19,7 @@ All integration tests run in an Electron instance. You can specify to run the te All integration tests run in a browser instance as specified by the command line arguments. Add the `--debug` flag to see a browser window with the tests running. + +## Debug + +All integration tests can be run and debugged from within VSCode (both Electron and Web) simply by selecting the related launch configuration and running them. diff --git a/test/integration/browser/package.json b/test/integration/browser/package.json index d43ee230c46..beb4142c55d 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 2ed75099d31..7972dd76521 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 35884dcfc51..126da8138c5 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 884cb5de667..d6e15c5e28d 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 4d25e3f82a9..e4f5accd3c7 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" @@ -9908,10 +9909,10 @@ xterm-addon-webgl@0.5.0: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.5.0.tgz#c1031dc7599cce3509824643ab5f15361c928e3e" integrity sha512-hQrvabKCnwXFaEZ+YtoJM9Pm0CIBXL5KSwoU+RiGStU3KYTAcqYP2GsH3dWdvKX6kTWhWLS81dtDsGkfbOciuA== -xterm@4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0.tgz#5915d3c4c8800fadbcf555a0a603c672ab9df589" - integrity sha512-JGIpigWM3EBWvnS3rtBuefkiToIILSK1HYMXy4BCsUpO+O4UeeV+/U1AdAXgCB6qJrnPNb7yLgBsVCQUNMteig== +xterm@4.5.0-beta.4: + version "4.5.0-beta.4" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.5.0-beta.4.tgz#701f05553b643236d3fcd8bb7f14045bd4537c92" + integrity sha512-Yv1Bf60LTLBMaig1rv033hPz8hQGXZN6VYW2oe/409t2NbJXPg5xZgf47qyaWFV7a5k1BFiwjayJCWaL2nYBew== y18n@^3.2.1: version "3.2.1"