diff --git a/extensions/git/package.json b/extensions/git/package.json index 4395d306bb..a6cf112f69 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1272,6 +1272,12 @@ "default": false, "description": "%config.fetchOnPull%" }, + "git.pullTags": { + "type": "boolean", + "scope": "resource", + "default": true, + "description": "%config.pullTags%" + }, "git.autoStash": { "type": "boolean", "scope": "resource", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 892e3519cd..90f3d82499 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -112,6 +112,7 @@ "config.rebaseWhenSync": "Force git to use rebase when running the sync command.", "config.confirmEmptyCommits": "Always confirm the creation of empty commits.", "config.fetchOnPull": "Fetch all branches when pulling or 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.", "config.useForcePushWithLease": "Controls whether force pushing uses the safer force-with-lease variant.", diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index b95f3c0f0a..3684b30c5a 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -12,7 +12,7 @@ import { EventEmitter } from 'events'; import iconv = require('iconv-lite'); import * as filetype from 'file-type'; import { assign, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent } from './util'; -import { CancellationToken, Uri } from 'vscode'; +import { CancellationToken, Uri, workspace } from 'vscode'; import { detectEncoding } from './encoding'; import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status } from './api/git'; @@ -1362,7 +1362,12 @@ export class Repository { } async pull(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise { - const args = ['pull', '--tags']; + const args = ['pull']; + const config = workspace.getConfiguration('git', Uri.file(this.root)); + + if (config.get('pullTags')) { + args.push('--tags'); + } if (options.unshallow) { args.push('--unshallow'); diff --git a/resources/win32/bin/code.sh b/resources/win32/bin/code.sh index 4dbf59f538..0ff31d69dd 100644 --- a/resources/win32/bin/code.sh +++ b/resources/win32/bin/code.sh @@ -10,40 +10,34 @@ VSCODE_PATH="$(dirname "$(dirname "$(realpath "$0")")")" ELECTRON="$VSCODE_PATH/$NAME.exe" if grep -qi Microsoft /proc/version; then # in a wsl shell - fallback() { + WSL_BUILD=$(uname -r | sed -E 's/^.+-([0-9]+)-[Mm]icrosoft/\1/') + if [ $WSL_BUILD -ge 17063 ] 2> /dev/null; then + # WSLPATH is available since WSL build 17046 + # WSLENV is available since WSL build 17063 + export WSLENV=ELECTRON_RUN_AS_NODE/w:$WSLENV + + # use the Remote WSL extension if installed + pushd "$VSCODE_PATH" > /dev/null + WSL_EXT_ID="ms-vscode-remote.remote-wsl" + WSL_EXT_WLOC=$(ELECTRON_RUN_AS_NODE=1 "$ELECTRON" ".\resources\app\out\cli.js" --locate-extension $WSL_EXT_ID) + popd > /dev/null + + if ! [ -z "$WSL_EXT_WLOC" ]; then + # replace \r\n with \n in WSL_EXT_WLOC + WSL_CODE=$(wslpath -u "${WSL_EXT_WLOC%%[[:cntrl:]]}")/scripts/wslCode.sh + WIN_CODE_CMD=$(wslpath -w "$VSCODE_PATH/bin/$APP_NAME.cmd") + "$WSL_CODE" $COMMIT $QUALITY "$WIN_CODE_CMD" "$APP_NAME" "$@" + exit $? + else + CLI=$(wslpath -m "$VSCODE_PATH/resources/app/out/cli.js") + fi + else # If running under older WSL, don't pass cli.js to Electron as # environment vars cannot be transferred from WSL to Windows # See: https://github.com/Microsoft/BashOnWindows/issues/1363 # https://github.com/Microsoft/BashOnWindows/issues/1494 "$ELECTRON" "$@" exit $? - } - WSL_BUILD=$(uname -r | sed -E 's/^.+-([0-9]+)-Microsoft/\1/') - # wslpath is not available prior to WSL build 17046 - # See: https://docs.microsoft.com/en-us/windows/wsl/release-notes#build-17046 - if [ -x /bin/wslpath ]; then - WIN_CODE_CMD=$(wslpath -w "$(dirname "$(realpath "$0")")/$APP_NAME.cmd") - # make sure the cwd is in the windows fs, otherwise there will be a warning from cmd - pushd "$(dirname "$0")" > /dev/null - WSL_EXT_ID="ms-vscode-remote.remote-wsl" - WSL_EXT_WLOC=$(cmd.exe /c "$WIN_CODE_CMD" --locate-extension $WSL_EXT_ID) - popd > /dev/null - if ! [ -z "$WSL_EXT_WLOC" ]; then - # replace \r\n with \n in WSL_EXT_WLOC, get linux path for - WSL_CODE=$(wslpath -u "${WSL_EXT_WLOC%%[[:cntrl:]]}")/scripts/wslCode.sh - "$WSL_CODE" $COMMIT $QUALITY "$WIN_CODE_CMD" "$APP_NAME" "$@" - exit $? - elif [ $WSL_BUILD -ge 17063 ] 2> /dev/null; then - # Since WSL build 17063, we just need to set WSLENV so that - # ELECTRON_RUN_AS_NODE is visible to the win32 process - # See: https://docs.microsoft.com/en-us/windows/wsl/release-notes#build-17063 - export WSLENV=ELECTRON_RUN_AS_NODE/w:$WSLENV - CLI=$(wslpath -m "$VSCODE_PATH/resources/app/out/cli.js") - else # $WSL_BUILD ∈ [17046, 17063) OR $WSL_BUILD is indeterminate - fallback "$@" - fi - else - fallback "$@" fi elif [ -x "$(command -v cygpath)" ]; then CLI=$(cygpath -m "$VSCODE_PATH/resources/app/out/cli.js") diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index 5729f405c2..bc28c7e3fd 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -242,6 +242,9 @@ export class ContextView extends Disposable { // if view intersects vertically with anchor, shift it horizontally if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) { horizontalAnchor.size = around.width; + if (anchorAlignment === AnchorAlignment.RIGHT) { + horizontalAnchor.offset = around.left; + } } const left = layout(window.innerWidth, viewSizeWidth, horizontalAnchor); diff --git a/src/vs/base/common/actions.ts b/src/vs/base/common/actions.ts index dca4a79f2c..8a75a27d5c 100644 --- a/src/vs/base/common/actions.ts +++ b/src/vs/base/common/actions.ts @@ -191,18 +191,19 @@ export class ActionRunner extends Disposable implements IActionRunner { private _onDidRun = this._register(new Emitter()); readonly onDidRun: Event = this._onDidRun.event; - run(action: IAction, context?: any): Promise { + async run(action: IAction, context?: any): Promise { if (!action.enabled) { return Promise.resolve(null); } this._onDidBeforeRun.fire({ action: action }); - return this.runAction(action, context).then((result: any) => { + try { + const result = await this.runAction(action, context); this._onDidRun.fire({ action: action, result: result }); - }, (error: any) => { + } catch (error) { this._onDidRun.fire({ action: action, error: error }); - }); + } } protected runAction(action: IAction, context?: any): Promise { diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index 3b990527ad..19c05ac0ae 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -362,6 +362,31 @@ export function matchesFuzzy2(pattern: string, word: string): IMatch[] | null { return score ? createMatches(score) : null; } +export function anyScore(pattern: string, lowPattern: string, _patternPos: number, word: string, lowWord: string, _wordPos: number): FuzzyScore { + const result = fuzzyScore(pattern, lowPattern, 0, word, lowWord, 0, true); + if (result) { + return result; + } + let matches = 0; + let score = 0; + let idx = _wordPos; + for (let patternPos = 0; patternPos < lowPattern.length && patternPos < _maxLen; ++patternPos) { + const wordPos = lowWord.indexOf(lowPattern.charAt(patternPos), idx); + if (wordPos >= 0) { + score += 1; + matches += 2 ** wordPos; + idx = wordPos + 1; + + } else if (matches !== 0) { + // once we have started matching things + // we need to match the remaining pattern + // characters + break; + } + } + return [score, matches, _wordPos]; +} + //#region --- fuzzyScore --- export function createMatches(score: undefined | FuzzyScore): IMatch[] { @@ -491,7 +516,7 @@ export namespace FuzzyScore { /** * No matches and value `-100` */ - export const Default: [-100, 0, 0] = [-100, 0, 0]; + export const Default: [-100, 0, 0] = <[-100, 0, 0]>Object.freeze([-100, 0, 0]); export function isDefault(score?: FuzzyScore): score is [-100, 0, 0] { return !score || (score[0] === -100 && score[1] === 0 && score[2] === 0); diff --git a/src/vs/base/node/config.ts b/src/vs/base/node/config.ts index aec9aa9ede..8ad630fc4f 100644 --- a/src/vs/base/node/config.ts +++ b/src/vs/base/node/config.ts @@ -9,7 +9,8 @@ import * as objects from 'vs/base/common/objects'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; -import { readlink, statLink } from 'vs/base/node/pfs'; +import { statLink } from 'vs/base/node/pfs'; +import { realpath } from 'vs/base/node/extpath'; import { watchFolder, watchFile } from 'vs/base/node/watcher'; export interface IConfigurationChangeEvent { @@ -130,7 +131,7 @@ export class ConfigWatcher implements IConfigWatcher, IDisposable { private async handleSymbolicLink(): Promise { const { stat, isSymbolicLink } = await statLink(this._path); if (isSymbolicLink && !stat.isDirectory()) { - const realPath = await readlink(this._path); + const realPath = await realpath(this._path); this.watch(realPath, false); } diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index bf908f49a0..a4c4af6635 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -213,10 +213,6 @@ export function symlink(target: string, path: string, type?: string): Promise { - return promisify(fs.readlink)(path); -} - export function truncate(path: string, len: number): Promise { return promisify(fs.truncate)(path, len); } diff --git a/src/vs/base/node/storage.ts b/src/vs/base/node/storage.ts index 3b12b4c8f4..9ea376abbc 100644 --- a/src/vs/base/node/storage.ts +++ b/src/vs/base/node/storage.ts @@ -554,10 +554,12 @@ export class SQLiteStorageDatabase implements IStorageDatabase { return integrity; } - private connect(path: string, retryOnBusy: boolean = true): Promise { + private async connect(path: string, retryOnBusy: boolean = true): Promise { this.logger.trace(`[storage ${this.name}] open(${path}, retryOnBusy: ${retryOnBusy})`); - return this.doConnect(path).then(undefined, error => { + try { + return await this.doConnect(path); + } catch (error) { this.logger.error(`[storage ${this.name}] open(): Unable to open DB due to ${error}`); // SQLITE_BUSY should only arise if another process is locking the same DB we want @@ -569,7 +571,9 @@ export class SQLiteStorageDatabase implements IStorageDatabase { // In this case we simply wait for some time and retry once to establish the connection. // if (error.code === 'SQLITE_BUSY' && retryOnBusy) { - return timeout(SQLiteStorageDatabase.BUSY_OPEN_TIMEOUT).then(() => this.connect(path, false /* not another retry */)); + await timeout(SQLiteStorageDatabase.BUSY_OPEN_TIMEOUT); + + return this.connect(path, false /* not another retry */); } // Otherwise, best we can do is to recover from a backup if that exists, as such we @@ -579,17 +583,19 @@ export class SQLiteStorageDatabase implements IStorageDatabase { // The final fallback is to use an in-memory DB which should only happen if the target // folder is really not writeable for us. // - return unlink(path) - .then(() => renameIgnoreError(this.toBackupPath(path), path)) - .then(() => this.doConnect(path)) - .then(undefined, error => { - this.logger.error(`[storage ${this.name}] open(): Unable to use backup due to ${error}`); + try { + await unlink(path); + await renameIgnoreError(this.toBackupPath(path), path); - // In case of any error to open the DB, use an in-memory - // DB so that we always have a valid DB to talk to. - return this.doConnect(SQLiteStorageDatabase.IN_MEMORY_PATH); - }); - }); + return await this.doConnect(path); + } catch (error) { + this.logger.error(`[storage ${this.name}] open(): Unable to use backup due to ${error}`); + + // In case of any error to open the DB, use an in-memory + // DB so that we always have a valid DB to talk to. + return this.doConnect(SQLiteStorageDatabase.IN_MEMORY_PATH); + } + } } private handleSQLiteError(connection: IDatabaseConnection, error: Error & { code?: string }, msg: string): void { diff --git a/src/vs/base/test/node/stream/stream.test.ts b/src/vs/base/test/node/stream/stream.test.ts index 9d68bf4cf5..b9e2d13a40 100644 --- a/src/vs/base/test/node/stream/stream.test.ts +++ b/src/vs/base/test/node/stream/stream.test.ts @@ -9,20 +9,19 @@ import * as stream from 'vs/base/node/stream'; import { getPathFromAmdModule } from 'vs/base/common/amd'; suite('Stream', () => { - test('readToMatchingString - ANSI', function () { + test('readToMatchingString - ANSI', async () => { const file = getPathFromAmdModule(require, './fixtures/file.css'); - return stream.readToMatchingString(file, '\n', 10, 100).then((result: string) => { - // \r may be present on Windows - assert.equal(result.replace('\r', ''), '/*---------------------------------------------------------------------------------------------'); - }); + const result = await stream.readToMatchingString(file, '\n', 10, 100); + + // \r may be present on Windows + assert.equal(result!.replace('\r', ''), '/*---------------------------------------------------------------------------------------------'); }); - test('readToMatchingString - empty', function () { + test('readToMatchingString - empty', async () => { const file = getPathFromAmdModule(require, './fixtures/empty.txt'); - return stream.readToMatchingString(file, '\n', 10, 100).then((result: string) => { - assert.equal(result, null); - }); + const result = await stream.readToMatchingString(file, '\n', 10, 100); + assert.equal(result, null); }); }); diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index 33ede75be4..b982aadc0b 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -42,7 +42,7 @@ import { Button } from 'vs/base/browser/ui/button/button'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { SystemInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnosticsService'; -const MAX_URL_LENGTH = platform.isWindows ? 2081 : 5400; +const MAX_URL_LENGTH = 2045; interface SearchResult { html_url: string; @@ -440,11 +440,11 @@ export class IssueReporter extends Disposable { } }); - document.onkeydown = (e: KeyboardEvent) => { + document.onkeydown = async (e: KeyboardEvent) => { const cmdOrCtrlKey = platform.isMacintosh ? e.metaKey : e.ctrlKey; // Cmd/Ctrl+Enter previews issue and closes window if (cmdOrCtrlKey && e.keyCode === 13) { - if (this.createIssue()) { + if (await this.createIssue()) { ipcRenderer.send('vscode:closeIssueReporter'); } } @@ -844,7 +844,7 @@ export class IssueReporter extends Disposable { return isValid; } - private createIssue(): boolean { + private async createIssue(): Promise { if (!this.validateInputs()) { // If inputs are invalid, set focus to the first one and add listeners on them // to detect further changes @@ -888,14 +888,32 @@ export class IssueReporter extends Disposable { let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`; if (url.length > MAX_URL_LENGTH) { - clipboard.writeText(issueBody); - url = baseUrl + `&body=${encodeURIComponent(localize('pasteData', "We have written the needed data into your clipboard because it was too large to send. Please paste."))}`; + try { + url = await this.writeToClipboard(baseUrl, issueBody); + } catch (_) { + return false; + } } ipcRenderer.send('vscode:openExternal', url); return true; } + private async writeToClipboard(baseUrl: string, issueBody: string): Promise { + return new Promise((resolve, reject) => { + ipcRenderer.once('vscode:issueReporterClipboardResponse', (_: unknown, shouldWrite: boolean) => { + if (shouldWrite) { + clipboard.writeText(issueBody); + resolve(baseUrl + `&body=${encodeURIComponent(localize('pasteData', "We have written the needed data into your clipboard because it was too large to send. Please paste."))}`); + } else { + reject(); + } + }); + + ipcRenderer.send('vscode:issueReporterClipboard'); + }); + } + private getExtensionGitHubUrl(): string { let repositoryUrl = ''; const bugsUrl = this.getExtensionBugsUrl(); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 471d1311ce..28b6572c9b 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -144,7 +144,7 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I } server.registerChannel('telemetryAppender', new TelemetryAppenderChannel(appInsightsAppender)); - services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService, [false])); + services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index d7d9ab796b..04c6304e0e 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -38,7 +38,6 @@ import { areSameExtensions, adoptToGalleryExtensionId, getGalleryExtensionId } f import { URI } from 'vs/base/common/uri'; import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { IExtensionManifest, ExtensionType, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; -import { isUIExtension } from 'vs/platform/extensions/node/extensionsUtil'; import { CancellationToken } from 'vs/base/common/cancellation'; import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; import { Schemas } from 'vs/base/common/network'; @@ -69,10 +68,8 @@ export function getIdAndVersion(id: string): [string, string | undefined] { export class Main { constructor( - private readonly remote: boolean, @IInstantiationService private readonly instantiationService: IInstantiationService, @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService ) { } @@ -138,10 +135,6 @@ export class Main { extension = path.isAbsolute(extension) ? extension : path.join(process.cwd(), extension); const manifest = await getManifest(extension); - if (this.remote && (!isLanguagePackExtension(manifest) && isUIExtension(manifest, [], this.configurationService))) { - console.log(localize('notSupportedUIExtension', "Can't install extension {0} since UI Extensions are not supported", getBaseLabel(extension))); - return null; - } const valid = await this.validate(manifest, force); if (valid) { @@ -180,11 +173,6 @@ export class Main { } const manifest = await this.extensionGalleryService.getManifest(extension, CancellationToken.None); - if (this.remote && manifest && (!isLanguagePackExtension(manifest) && isUIExtension(manifest, [], this.configurationService))) { - console.log(localize('notSupportedUIExtension', "Can't install extension {0} since UI Extensions are not supported", extension.identifier.id)); - return null; - } - const [installedExtension] = installed.filter(e => areSameExtensions(e.identifier, { id })); if (installedExtension) { if (extension.version === installedExtension.manifest.version) { @@ -312,7 +300,7 @@ export function main(argv: ParsedArgs): Promise { const services = new ServiceCollection(); services.set(IConfigurationService, new SyncDescriptor(ConfigurationService, [environmentService.appSettingsPath])); services.set(IRequestService, new SyncDescriptor(RequestService)); - services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService, [false])); + services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); const appenders: AppInsightsAppender[] = []; @@ -334,7 +322,7 @@ export function main(argv: ParsedArgs): Promise { } const instantiationService2 = instantiationService.createChild(services); - const main = instantiationService2.createInstance(Main, false); + const main = instantiationService2.createInstance(Main); return main.run(argv).then(() => { // Dispose the AI adapter so that remaining data gets flushed. diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index ed954f5023..ca1acb961f 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1288,6 +1288,7 @@ export interface CommentThread2 { resource: string | null; range: IRange; label: string; + contextValue: string | undefined; comments: Comment[] | undefined; onDidChangeComments: Event; collapsibleState?: CommentThreadCollapsibleState; @@ -1326,6 +1327,7 @@ export interface CommentThread { collapsibleState?: CommentThreadCollapsibleState; reply?: Command; isDisposed?: boolean; + contextValue?: string; } /** @@ -1347,14 +1349,24 @@ export interface CommentReaction { readonly canEdit?: boolean; } +/** + * @internal + */ +export enum CommentMode { + Editing = 0, + Preview = 1 +} + /** * @internal */ export interface Comment { readonly commentId: string; + readonly uniqueIdInThread?: number; readonly body: IMarkdownString; readonly userName: string; readonly userIconPath?: string; + readonly contextValue?: string; readonly canEdit?: boolean; readonly canDelete?: boolean; readonly selectCommand?: Command; @@ -1363,6 +1375,7 @@ export interface Comment { readonly isDraft?: boolean; readonly commentReactions?: CommentReaction[]; readonly label?: string; + readonly mode?: CommentMode; } /** diff --git a/src/vs/editor/contrib/format/format.ts b/src/vs/editor/contrib/format/format.ts index eff84dac81..162c710ec7 100644 --- a/src/vs/editor/contrib/format/format.ts +++ b/src/vs/editor/contrib/format/format.ts @@ -152,17 +152,22 @@ export async function formatDocumentRangeWithProvider( cts = new TextModelCancellationTokenSource(editorOrModel, token); } - const rawEdits = await provider.provideDocumentRangeFormattingEdits( - model, - range, - model.getFormattingOptions(), - cts.token - ); + let edits: TextEdit[] | undefined; + try { + const rawEdits = await provider.provideDocumentRangeFormattingEdits( + model, + range, + model.getFormattingOptions(), + cts.token + ); + edits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits); - const edits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits); + if (cts.token.isCancellationRequested) { + return true; + } - if (cts.token.isCancellationRequested) { - return true; + } finally { + cts.dispose(); } if (!edits || edits.length === 0) { @@ -235,16 +240,22 @@ export async function formatDocumentWithProvider( cts = new TextModelCancellationTokenSource(editorOrModel, token); } - const rawEdits = await provider.provideDocumentFormattingEdits( - model, - model.getFormattingOptions(), - cts.token - ); + let edits: TextEdit[] | undefined; + try { + const rawEdits = await provider.provideDocumentFormattingEdits( + model, + model.getFormattingOptions(), + cts.token + ); - const edits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits); + edits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits); - if (cts.token.isCancellationRequested) { - return true; + if (cts.token.isCancellationRequested) { + return true; + } + + } finally { + cts.dispose(); } if (!edits || edits.length === 0) { diff --git a/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts b/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts index 33003b3a01..68f2358f4f 100644 --- a/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts +++ b/src/vs/editor/contrib/goToDefinition/goToDefinitionCommands.ts @@ -29,6 +29,7 @@ import { IProgressService } from 'vs/platform/progress/common/progress'; import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefinitionsAtPosition, getDeclarationsAtPosition } from './goToDefinition'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { EditorStateCancellationTokenSource, CodeEditorStateFlag } from 'vs/editor/browser/core/editorState'; +import { ISymbolNavigationService } from 'vs/editor/contrib/goToDefinition/goToDefinitionResultsNavigation'; export class DefinitionActionConfig { @@ -58,6 +59,7 @@ export class DefinitionAction extends EditorAction { const notificationService = accessor.get(INotificationService); const editorService = accessor.get(ICodeEditorService); const progressService = accessor.get(IProgressService); + const symbolNavService = accessor.get(ISymbolNavigationService); const model = editor.getModel(); const pos = editor.getPosition(); @@ -102,7 +104,7 @@ export class DefinitionAction extends EditorAction { } else { // handle multile results - return this._onResult(editorService, editor, new ReferencesModel(result)); + return this._onResult(editorService, symbolNavService, editor, new ReferencesModel(result)); } }, (err) => { @@ -130,7 +132,7 @@ export class DefinitionAction extends EditorAction { return model.references.length > 1 ? nls.localize('meta.title', " – {0} definitions", model.references.length) : ''; } - private async _onResult(editorService: ICodeEditorService, editor: ICodeEditor, model: ReferencesModel): Promise { + private async _onResult(editorService: ICodeEditorService, symbolNavService: ISymbolNavigationService, editor: ICodeEditor, model: ReferencesModel): Promise { const msg = model.getAriaMessage(); alert(msg); @@ -150,6 +152,12 @@ export class DefinitionAction extends EditorAction { } else { model.dispose(); } + + // keep remaining locations around when using + // 'goto'-mode + if (gotoLocation.multiple === 'goto') { + symbolNavService.put(next); + } } } diff --git a/src/vs/editor/contrib/goToDefinition/goToDefinitionResultsNavigation.ts b/src/vs/editor/contrib/goToDefinition/goToDefinitionResultsNavigation.ts new file mode 100644 index 0000000000..b3d90178f0 --- /dev/null +++ b/src/vs/editor/contrib/goToDefinition/goToDefinitionResultsNavigation.ts @@ -0,0 +1,212 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ReferencesModel, OneReference } from 'vs/editor/contrib/referenceSearch/referencesModel'; +import { RawContextKey, IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { registerEditorCommand, EditorCommand } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { Range } from 'vs/editor/common/core/range'; +import { Disposable, dispose, combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; +import { localize } from 'vs/nls'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; + +export const ctxHasSymbols = new RawContextKey('hasSymbols', false); + +export const ISymbolNavigationService = createDecorator('ISymbolNavigationService'); + +export interface ISymbolNavigationService { + _serviceBrand: any; + reset(): void; + put(anchor: OneReference): void; + revealNext(source: ICodeEditor): Promise; +} + +class SymbolNavigationService implements ISymbolNavigationService { + + _serviceBrand: any; + + private readonly _ctxHasSymbols: IContextKey; + + private _currentModel?: ReferencesModel = undefined; + private _currentIdx: number = -1; + private _currentDisposables: IDisposable[] = []; + private _currentMessage?: IDisposable = undefined; + private _ignoreEditorChange: boolean = false; + + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICodeEditorService private readonly _editorService: ICodeEditorService, + @IStatusbarService private readonly _statusbarService: IStatusbarService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + ) { + this._ctxHasSymbols = ctxHasSymbols.bindTo(contextKeyService); + } + + reset(): void { + this._ctxHasSymbols.reset(); + dispose(this._currentDisposables); + dispose(this._currentMessage); + this._currentModel = undefined; + this._currentIdx = -1; + } + + put(anchor: OneReference): void { + const refModel = anchor.parent.parent; + + if (refModel.references.length <= 1) { + this.reset(); + return; + } + + this._currentModel = refModel; + this._currentIdx = refModel.references.indexOf(anchor); + this._ctxHasSymbols.set(true); + this._showMessage(); + + const editorStatus = new EditorStatus(this._editorService); + const listener = editorStatus.onDidChange(_ => { + + if (this._ignoreEditorChange) { + return; + } + + const editor = this._editorService.getActiveCodeEditor(); + if (!editor) { + return; + } + const model = editor.getModel(); + const position = editor.getPosition(); + if (!model || !position) { + return; + } + + let seenUri: boolean = false; + let seenPosition: boolean = false; + for (const reference of refModel.references) { + if (reference.uri.toString() === model.uri.toString()) { + seenUri = true; + seenPosition = seenPosition || Range.containsPosition(reference.range, position); + } else if (seenUri) { + break; + } + } + if (!seenUri || !seenPosition) { + this.reset(); + } + }); + + this._currentDisposables = [editorStatus, listener]; + } + + revealNext(source: ICodeEditor): Promise { + if (!this._currentModel) { + return Promise.resolve(); + } + + // get next result and advance + this._currentIdx += 1; + this._currentIdx %= this._currentModel.references.length; + const reference = this._currentModel.references[this._currentIdx]; + + // status + this._showMessage(); + + // open editor, ignore events while that happens + this._ignoreEditorChange = true; + return this._editorService.openCodeEditor({ + resource: reference.uri, + options: { + selection: Range.collapseToStart(reference.range), + revealInCenterIfOutsideViewport: true, + revealIfOpened: true + } + }, source).finally(() => { + this._ignoreEditorChange = false; + }); + + } + + private _showMessage(): void { + + dispose(this._currentMessage); + + const kb = this._keybindingService.lookupKeybinding('editor.gotoNextSymbolFromResult'); + const message = kb + ? localize('location.kb', "Symbol {0} of {1}, press {2} to reveal next", this._currentIdx + 1, this._currentModel!.references.length, kb.getLabel()) + : localize('location', "Symbol {0} of {1}", this._currentIdx + 1, this._currentModel!.references.length); + + this._currentMessage = this._statusbarService.setStatusMessage(message); + } +} + +registerSingleton(ISymbolNavigationService, SymbolNavigationService, true); + +registerEditorCommand(new class extends EditorCommand { + + constructor() { + super({ + id: 'editor.gotoNextSymbolFromResult', + precondition: ContextKeyExpr.and( + ctxHasSymbols, + ContextKeyExpr.equals('config.editor.gotoLocation.multiple', 'goto') + ), + kbOpts: { + weight: KeybindingWeight.EditorContrib, + primary: KeyCode.F12 + } + }); + } + + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise { + return accessor.get(ISymbolNavigationService).revealNext(editor); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'editor.gotoNextSymbolFromResult.cancel', + weight: KeybindingWeight.EditorContrib, + when: ctxHasSymbols, + primary: KeyCode.Escape, + handler(accessor) { + accessor.get(ISymbolNavigationService).reset(); + } +}); + +// + +class EditorStatus extends Disposable { + + private readonly _listener = new Map(); + + private readonly _onDidChange = new Emitter<{ editor: ICodeEditor }>(); + readonly onDidChange: Event<{ editor: ICodeEditor }> = this._onDidChange.event; + + constructor(@ICodeEditorService editorService: ICodeEditorService) { + super(); + this._register(this._onDidChange); + this._register(editorService.onCodeEditorRemove(this._onDidRemoveEditor, this)); + this._register(editorService.onCodeEditorAdd(this._onDidAddEditor, this)); + editorService.listCodeEditors().forEach(this._onDidAddEditor, this); + } + + private _onDidAddEditor(editor: ICodeEditor): void { + this._listener.set(editor, combinedDisposable([ + editor.onDidChangeCursorPosition(_ => this._onDidChange.fire({ editor })), + editor.onDidChangeModelContent(_ => this._onDidChange.fire({ editor })), + ])); + } + + private _onDidRemoveEditor(editor: ICodeEditor): void { + dispose(this._listener.get(editor)); + this._listener.delete(editor); + } +} diff --git a/src/vs/editor/contrib/suggest/completionModel.ts b/src/vs/editor/contrib/suggest/completionModel.ts index 461a2053f9..1e33eb7261 100644 --- a/src/vs/editor/contrib/suggest/completionModel.ts +++ b/src/vs/editor/contrib/suggest/completionModel.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { fuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScorer, FuzzyScore } from 'vs/base/common/filters'; +import { fuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScorer, FuzzyScore, anyScore } from 'vs/base/common/filters'; import { isDisposable } from 'vs/base/common/lifecycle'; import { CompletionList, CompletionItemProvider, CompletionItemKind } from 'vs/editor/common/modes'; import { CompletionItem } from './suggest'; @@ -222,7 +222,7 @@ export class CompletionModel { } else { // re-run the scorer on the label in the hope of a result BUT use the rank // of the filterText-match - item.score = scoreFn(word, wordLow, wordPos, item.completion.label, item.labelLow, 0, false) || FuzzyScore.Default; + item.score = anyScore(word, wordLow, wordPos, item.completion.label, item.labelLow, 0); item.score[0] = match[0]; // use score from filterText } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 396e49d6a8..bf98794d33 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -100,6 +100,10 @@ export const enum MenuId { NotebookToolbar, DataExplorerContext, DataExplorerAction, + CommentThreadTitle, + CommentThreadActions, + CommentTitle, + CommentActions } export interface IMenuActionOptions { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 1da7dea559..62f9c97fc7 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -8,7 +8,7 @@ import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; import { assign } from 'vs/base/common/objects'; import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { flatten } from 'vs/base/common/arrays'; +import { flatten, isNonEmptyArray } from 'vs/base/common/arrays'; import { extract, ExtractError, zip, IFile } from 'vs/base/node/zip'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, @@ -44,9 +44,7 @@ import { Schemas } from 'vs/base/common/network'; import { CancellationToken } from 'vs/base/common/cancellation'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; -import { IExtensionManifest, ExtensionType, ExtensionIdentifierWithVersion, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { isUIExtension } from 'vs/platform/extensions/node/extensionsUtil'; +import { IExtensionManifest, ExtensionType, ExtensionIdentifierWithVersion } from 'vs/platform/extensions/common/extensions'; // {{SQL CARBON EDIT} import product from 'vs/platform/product/node/product'; @@ -132,9 +130,7 @@ export class ExtensionManagementService extends Disposable implements IExtension onDidUninstallExtension: Event = this._onDidUninstallExtension.event; constructor( - private readonly remote: boolean, @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @ILogService private readonly logService: ILogService, @optional(IDownloadService) private downloadService: IDownloadService, @@ -270,7 +266,18 @@ export class ExtensionManagementService extends Disposable implements IExtension private installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IGalleryMetadata | null, type: ExtensionType, operation: InstallOperation, token: CancellationToken): Promise { return this.toNonCancellablePromise(this.installExtension({ zipPath, identifierWithVersion, metadata }, type, token) - .then(local => this.installDependenciesAndPackExtensions(local, null).then(() => local, error => this.uninstall(local, true).then(() => Promise.reject(error), () => Promise.reject(error)))) + .then(local => this.installDependenciesAndPackExtensions(local, null) + .then( + () => local, + error => { + if (isNonEmptyArray(local.manifest.extensionDependencies)) { + this.logService.warn(`Cannot install dependencies of extension:`, local.identifier.id, error.message); + } + if (isNonEmptyArray(local.manifest.extensionPack)) { + this.logService.warn(`Cannot install packed extensions of extension:`, local.identifier.id, error.message); + } + return local; + })) .then( local => { this._onDidInstallExtension.fire({ identifier: identifierWithVersion.identifier, zipPath, local, operation }); return local; }, error => { this._onDidInstallExtension.fire({ identifier: identifierWithVersion.identifier, zipPath, operation, error }); return Promise.reject(error); } @@ -526,16 +533,7 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length }, CancellationToken.None) .then(galleryResult => { const extensionsToInstall = galleryResult.firstPage; - return Promise.all(extensionsToInstall.map(async e => { - if (this.remote) { - const manifest = await this.galleryService.getManifest(e, CancellationToken.None); - if (manifest && isUIExtension(manifest, [], this.configurationService) && !isLanguagePackExtension(manifest)) { - this.logService.info('Ignored installing the UI dependency', e.identifier.id); - return; - } - } - return this.installFromGallery(e); - })) + return Promise.all(extensionsToInstall.map(e => this.installFromGallery(e))) .then(() => null, errors => this.rollback(extensionsToInstall).then(() => Promise.reject(errors), () => Promise.reject(errors))); }); } diff --git a/src/vs/platform/extensions/node/extensionsUtil.ts b/src/vs/platform/extensions/node/extensionsUtil.ts deleted file mode 100644 index 33aab8128b..0000000000 --- a/src/vs/platform/extensions/node/extensionsUtil.ts +++ /dev/null @@ -1,51 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -import { getGalleryExtensionId, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { isNonEmptyArray } from 'vs/base/common/arrays'; -import product from 'vs/platform/product/node/product'; - -export function isUIExtension(manifest: IExtensionManifest, uiContributions: string[], configurationService: IConfigurationService): boolean { - const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); - const extensionKind = getExtensionKind(manifest, configurationService); - switch (extensionKind) { - case 'ui': return true; - case 'workspace': return false; - default: { - // Tagged as UI extension in product - if (isNonEmptyArray(product.uiExtensions) && product.uiExtensions.some(id => areSameExtensions({ id }, { id: extensionId }))) { - return true; - } - // Not an UI extension if it has main - if (manifest.main) { - return false; - } - // Not an UI extension if it has dependencies or an extension pack - if (isNonEmptyArray(manifest.extensionDependencies) || isNonEmptyArray(manifest.extensionPack)) { - return false; - } - if (manifest.contributes) { - // Not an UI extension if it has no ui contributions - if (!uiContributions.length || Object.keys(manifest.contributes).some(contribution => uiContributions.indexOf(contribution) === -1)) { - return false; - } - } - return true; - } - } -} - -function getExtensionKind(manifest: IExtensionManifest, configurationService: IConfigurationService): string | undefined { - const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); - const configuredExtensionKinds = configurationService.getValue<{ [key: string]: string }>('remote.extensionKind') || {}; - for (const id of Object.keys(configuredExtensionKinds)) { - if (areSameExtensions({ id: extensionId }, { id })) { - return configuredExtensionKinds[id]; - } - } - return manifest.extensionKind; -} diff --git a/src/vs/platform/issue/electron-main/issueService.ts b/src/vs/platform/issue/electron-main/issueService.ts index f867b27de4..260b1d253d 100644 --- a/src/vs/platform/issue/electron-main/issueService.ts +++ b/src/vs/platform/issue/electron-main/issueService.ts @@ -75,6 +75,23 @@ export class IssueService implements IIssueService { event.sender.send('vscode:listProcessesResponse', processes); }); + ipcMain.on('vscode:issueReporterClipboard', (event: Event) => { + const messageOptions = { + message: localize('issueReporterWriteToClipboard', "There is too much data to send to GitHub. Would you like to write the information to the clipboard so that it can be pasted?"), + type: 'warning', + buttons: [ + localize('yes', "Yes"), + localize('cancel', "Cancel") + ] + }; + + if (this._issueWindow) { + dialog.showMessageBox(this._issueWindow, messageOptions, response => { + event.sender.send('vscode:issueReporterClipboardResponse', response === 0); + }); + } + }); + ipcMain.on('vscode:issuePerformanceInfoRequest', (event: Event) => { this.getPerformanceInfo().then(msg => { event.sender.send('vscode:issuePerformanceInfoResponse', msg); diff --git a/src/vs/platform/lifecycle/common/lifecycleService.ts b/src/vs/platform/lifecycle/common/lifecycleService.ts index cb9cb88447..6db38f29df 100644 --- a/src/vs/platform/lifecycle/common/lifecycleService.ts +++ b/src/vs/platform/lifecycle/common/lifecycleService.ts @@ -58,9 +58,9 @@ export abstract class AbstractLifecycleService extends Disposable implements ILi } } - when(phase: LifecyclePhase): Promise { + async when(phase: LifecyclePhase): Promise { if (phase <= this._phase) { - return Promise.resolve(); + return; } let barrier = this.phaseWhen.get(phase); @@ -69,6 +69,6 @@ export abstract class AbstractLifecycleService extends Disposable implements ILi this.phaseWhen.set(phase, barrier); } - return barrier.wait().then(undefined); + await barrier.wait(); } } diff --git a/src/vs/platform/lifecycle/electron-browser/lifecycleService.ts b/src/vs/platform/lifecycle/electron-browser/lifecycleService.ts index 6c6d033adb..a323e2eb98 100644 --- a/src/vs/platform/lifecycle/electron-browser/lifecycleService.ts +++ b/src/vs/platform/lifecycle/electron-browser/lifecycleService.ts @@ -75,18 +75,17 @@ export class LifecycleService extends AbstractLifecycleService { }); // Main side indicates that we will indeed shutdown - ipc.on('vscode:onWillUnload', (_event: unknown, reply: { replyChannel: string, reason: ShutdownReason }) => { + ipc.on('vscode:onWillUnload', async (_event: unknown, reply: { replyChannel: string, reason: ShutdownReason }) => { this.logService.trace(`lifecycle: onWillUnload (reason: ${reply.reason})`); // trigger onWillShutdown events and joining - return this.handleWillShutdown(reply.reason).then(() => { + await this.handleWillShutdown(reply.reason); - // trigger onShutdown event now that we know we will quit - this._onShutdown.fire(); + // trigger onShutdown event now that we know we will quit + this._onShutdown.fire(); - // acknowledge to main side - ipc.send(reply.replyChannel, windowId); - }); + // acknowledge to main side + ipc.send(reply.replyChannel, windowId); }); // Save shutdown reason to retrieve on next startup @@ -111,7 +110,7 @@ export class LifecycleService extends AbstractLifecycleService { }); } - private handleWillShutdown(reason: ShutdownReason): Promise { + private async handleWillShutdown(reason: ShutdownReason): Promise { const joiners: Promise[] = []; this._onWillShutdown.fire({ @@ -123,9 +122,11 @@ export class LifecycleService extends AbstractLifecycleService { reason }); - return Promise.all(joiners).then(() => undefined, err => { - this.notificationService.error(toErrorMessage(err)); - onUnexpectedError(err); - }); + try { + await Promise.all(joiners); + } catch (error) { + this.notificationService.error(toErrorMessage(error)); + onUnexpectedError(error); + } } } diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts index ab3b3cd757..3c53a936cb 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts @@ -291,7 +291,7 @@ export class LifecycleService extends Disposable implements ILifecycleService { }); } - unload(window: ICodeWindow, reason: UnloadReason): Promise { + async unload(window: ICodeWindow, reason: UnloadReason): Promise { // Always allow to unload a window that is not yet ready if (!window.isReady) { @@ -302,27 +302,27 @@ export class LifecycleService extends Disposable implements ILifecycleService { // first ask the window itself if it vetos the unload const windowUnloadReason = this._quitRequested ? UnloadReason.QUIT : reason; - return this.onBeforeUnloadWindowInRenderer(window, windowUnloadReason).then(veto => { - if (veto) { - this.logService.trace(`Lifecycle#unload() - veto in renderer (window ID ${window.id})`); + let veto = await this.onBeforeUnloadWindowInRenderer(window, windowUnloadReason); + if (veto) { + this.logService.trace(`Lifecycle#unload() - veto in renderer (window ID ${window.id})`); - return this.handleWindowUnloadVeto(veto); - } + return this.handleWindowUnloadVeto(veto); + } - // then check for vetos in the main side - return this.onBeforeUnloadWindowInMain(window, windowUnloadReason).then(veto => { - if (veto) { - this.logService.trace(`Lifecycle#unload() - veto in main (window ID ${window.id})`); + // then check for vetos in the main side + veto = await this.onBeforeUnloadWindowInMain(window, windowUnloadReason); + if (veto) { + this.logService.trace(`Lifecycle#unload() - veto in main (window ID ${window.id})`); - return this.handleWindowUnloadVeto(veto); - } + return this.handleWindowUnloadVeto(veto); + } - this.logService.trace(`Lifecycle#unload() - no veto (window ID ${window.id})`); + this.logService.trace(`Lifecycle#unload() - no veto (window ID ${window.id})`); - // finally if there are no vetos, unload the renderer - return this.onWillUnloadWindowInRenderer(window, windowUnloadReason).then(() => false); - }); - }); + // finally if there are no vetos, unload the renderer + await this.onWillUnloadWindowInRenderer(window, windowUnloadReason); + + return false; } private handleWindowUnloadVeto(veto: boolean): boolean { diff --git a/src/vs/platform/state/node/stateService.ts b/src/vs/platform/state/node/stateService.ts index af82bf3107..b853abd108 100644 --- a/src/vs/platform/state/node/stateService.ts +++ b/src/vs/platform/state/node/stateService.ts @@ -26,21 +26,23 @@ export class FileStorage { return this._database; } - init(): Promise { - return readFile(this.dbPath).then(contents => { + async init(): Promise { + try { + const contents = await readFile(this.dbPath); + try { this.lastFlushedSerializedDatabase = contents.toString(); this._database = JSON.parse(this.lastFlushedSerializedDatabase); } catch (error) { this._database = {}; } - }, error => { + } catch (error) { if (error.code !== 'ENOENT') { this.onError(error); } this._database = {}; - }); + } } private loadSync(): object { diff --git a/src/vs/platform/state/test/node/state.test.ts b/src/vs/platform/state/test/node/state.test.ts index a0abe8dda7..b84d83074a 100644 --- a/src/vs/platform/state/test/node/state.test.ts +++ b/src/vs/platform/state/test/node/state.test.ts @@ -14,38 +14,37 @@ suite('StateService', () => { const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'stateservice'); const storageFile = path.join(parentDir, 'storage.json'); - teardown(done => { - rimraf(parentDir, RimRafMode.MOVE).then(done, done); + teardown(async () => { + await rimraf(parentDir, RimRafMode.MOVE); }); - test('Basics', () => { - return mkdirp(parentDir).then(() => { - writeFileSync(storageFile, ''); + test('Basics', async () => { + await mkdirp(parentDir); + writeFileSync(storageFile, ''); - let service = new FileStorage(storageFile, () => null); + let service = new FileStorage(storageFile, () => null); - service.setItem('some.key', 'some.value'); - assert.equal(service.getItem('some.key'), 'some.value'); + service.setItem('some.key', 'some.value'); + assert.equal(service.getItem('some.key'), 'some.value'); - service.removeItem('some.key'); - assert.equal(service.getItem('some.key', 'some.default'), 'some.default'); + service.removeItem('some.key'); + assert.equal(service.getItem('some.key', 'some.default'), 'some.default'); - assert.ok(!service.getItem('some.unknonw.key')); + assert.ok(!service.getItem('some.unknonw.key')); - service.setItem('some.other.key', 'some.other.value'); + service.setItem('some.other.key', 'some.other.value'); - service = new FileStorage(storageFile, () => null); + service = new FileStorage(storageFile, () => null); - assert.equal(service.getItem('some.other.key'), 'some.other.value'); + assert.equal(service.getItem('some.other.key'), 'some.other.value'); - service.setItem('some.other.key', 'some.other.value'); - assert.equal(service.getItem('some.other.key'), 'some.other.value'); + service.setItem('some.other.key', 'some.other.value'); + assert.equal(service.getItem('some.other.key'), 'some.other.value'); - service.setItem('some.undefined.key', undefined); - assert.equal(service.getItem('some.undefined.key', 'some.default'), 'some.default'); + service.setItem('some.undefined.key', undefined); + assert.equal(service.getItem('some.undefined.key', 'some.default'), 'some.default'); - service.setItem('some.null.key', null); - assert.equal(service.getItem('some.null.key', 'some.default'), 'some.default'); - }); + service.setItem('some.null.key', null); + assert.equal(service.getItem('some.null.key', 'some.default'), 'some.default'); }); }); \ No newline at end of file diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts index 18a91707e8..f141b73b12 100644 --- a/src/vs/platform/storage/node/storageIpc.ts +++ b/src/vs/platform/storage/node/storageIpc.ts @@ -45,20 +45,21 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC this.whenReady = this.init(); } - private init(): Promise { - return this.storageMainService.initialize().then(undefined, error => { + private async init(): Promise { + try { + await this.storageMainService.initialize(); + } catch (error) { onUnexpectedError(error); this.logService.error(error); - }).then(() => { + } - // Apply global telemetry values as part of the initialization - // These are global across all windows and thereby should be - // written from the main process once. - this.initTelemetry(); + // Apply global telemetry values as part of the initialization + // These are global across all windows and thereby should be + // written from the main process once. + this.initTelemetry(); - // Setup storage change listeners - this.registerListeners(); - }); + // Setup storage change listeners + this.registerListeners(); } private initTelemetry(): void { @@ -112,33 +113,39 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC throw new Error(`Event not found: ${event}`); } - call(_: unknown, command: string, arg?: any): Promise { + async call(_: unknown, command: string, arg?: any): Promise { + + // ensure to always wait for ready + await this.whenReady; + + // handle call switch (command) { case 'getItems': { - return this.whenReady.then(() => mapToSerializable(this.storageMainService.items)); + return mapToSerializable(this.storageMainService.items); } case 'updateItems': { - return this.whenReady.then(() => { - const items: ISerializableUpdateRequest = arg; - if (items.insert) { - for (const [key, value] of items.insert) { - this.storageMainService.store(key, value); - } + const items: ISerializableUpdateRequest = arg; + if (items.insert) { + for (const [key, value] of items.insert) { + this.storageMainService.store(key, value); } + } - if (items.delete) { - items.delete.forEach(key => this.storageMainService.remove(key)); - } - }); + if (items.delete) { + items.delete.forEach(key => this.storageMainService.remove(key)); + } + + break; } case 'checkIntegrity': { - return this.whenReady.then(() => this.storageMainService.checkIntegrity(arg)); + return this.storageMainService.checkIntegrity(arg); } - } - throw new Error(`Call not found: ${command}`); + default: + throw new Error(`Call not found: ${command}`); + } } } @@ -167,8 +174,10 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS } } - getItems(): Promise> { - return this.channel.call('getItems').then((data: Item[]) => serializableToMap(data)); + async getItems(): Promise> { + const items: Item[] = await this.channel.call('getItems'); + + return serializableToMap(items); } updateItems(request: IUpdateRequest): Promise { diff --git a/src/vs/platform/storage/node/storageMainService.ts b/src/vs/platform/storage/node/storageMainService.ts index f14ecfb7c2..f8001e3197 100644 --- a/src/vs/platform/storage/node/storageMainService.ts +++ b/src/vs/platform/storage/node/storageMainService.ts @@ -10,7 +10,6 @@ import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IStorage, Storage, SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions, InMemoryStorageDatabase } from 'vs/base/node/storage'; import { join } from 'vs/base/common/path'; -import { exists } from 'vs/base/node/pfs'; export const IStorageMainService = createDecorator('storageMainService'); @@ -121,25 +120,14 @@ export class StorageMainService extends Disposable implements IStorageMainServic } private doInitialize(): Promise { - const useInMemoryStorage = this.storagePath === SQLiteStorageDatabase.IN_MEMORY_PATH; + this.storage.dispose(); + this.storage = new Storage(new SQLiteStorageDatabase(this.storagePath, { + logging: this.createLogginOptions() + })); - let globalStorageExists: Promise; - if (useInMemoryStorage) { - globalStorageExists = Promise.resolve(true); - } else { - globalStorageExists = exists(this.storagePath); - } + this._register(this.storage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key }))); - return globalStorageExists.then(exists => { - this.storage.dispose(); - this.storage = new Storage(new SQLiteStorageDatabase(this.storagePath, { - logging: this.createLogginOptions() - })); - - this._register(this.storage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key }))); - - return this.storage.init(); - }); + return this.storage.init(); } get(key: string, fallbackValue: string): string; diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index f4cdc631db..12d128f075 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -62,38 +62,38 @@ export class StorageService extends Disposable implements IStorageService { return this.initializePromise; } - private doInitialize(payload: IWorkspaceInitializationPayload): Promise { - return Promise.all([ + private async doInitialize(payload: IWorkspaceInitializationPayload): Promise { + await Promise.all([ this.initializeGlobalStorage(), this.initializeWorkspaceStorage(payload) - ]).then(() => undefined); + ]); } private initializeGlobalStorage(): Promise { return this.globalStorage.init(); } - private initializeWorkspaceStorage(payload: IWorkspaceInitializationPayload): Promise { + private async initializeWorkspaceStorage(payload: IWorkspaceInitializationPayload): Promise { // Prepare workspace storage folder for DB - return this.prepareWorkspaceStorageFolder(payload).then(result => { + try { + const result = await this.prepareWorkspaceStorageFolder(payload); + const useInMemoryStorage = !!this.environmentService.extensionTestsLocationURI; // no storage during extension tests! // Create workspace storage and initalize mark('willInitWorkspaceStorage'); - return this.createWorkspaceStorage(useInMemoryStorage ? SQLiteStorageDatabase.IN_MEMORY_PATH : join(result.path, StorageService.WORKSPACE_STORAGE_NAME), result.wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined).init().then(() => { + try { + await this.createWorkspaceStorage(useInMemoryStorage ? SQLiteStorageDatabase.IN_MEMORY_PATH : join(result.path, StorageService.WORKSPACE_STORAGE_NAME), result.wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined).init(); + } finally { mark('didInitWorkspaceStorage'); - }, error => { - mark('didInitWorkspaceStorage'); - - return Promise.reject(error); - }); - }).then(undefined, error => { + } + } catch (error) { onUnexpectedError(error); // Upon error, fallback to in-memory storage return this.createWorkspaceStorage(SQLiteStorageDatabase.IN_MEMORY_PATH).init(); - }); + } } private createWorkspaceStorage(workspaceStoragePath: string, hint?: StorageHint): IStorage { @@ -120,22 +120,20 @@ export class StorageService extends Disposable implements IStorageService { return join(this.environmentService.workspaceStorageHome, payload.id); // workspace home + workspace id; } - private prepareWorkspaceStorageFolder(payload: IWorkspaceInitializationPayload): Promise<{ path: string, wasCreated: boolean }> { + private async prepareWorkspaceStorageFolder(payload: IWorkspaceInitializationPayload): Promise<{ path: string, wasCreated: boolean }> { const workspaceStorageFolderPath = this.getWorkspaceStorageFolderPath(payload); - return exists(workspaceStorageFolderPath).then<{ path: string, wasCreated: boolean }>(exists => { - if (exists) { - return { path: workspaceStorageFolderPath, wasCreated: false }; - } + const storageExists = await exists(workspaceStorageFolderPath); + if (storageExists) { + return { path: workspaceStorageFolderPath, wasCreated: false }; + } - return mkdirp(workspaceStorageFolderPath).then(() => { + await mkdirp(workspaceStorageFolderPath); - // Write metadata into folder - this.ensureWorkspaceStorageFolderMeta(payload); + // Write metadata into folder + this.ensureWorkspaceStorageFolderMeta(payload); - return { path: workspaceStorageFolderPath, wasCreated: true }; - }); - }); + return { path: workspaceStorageFolderPath, wasCreated: true }; } private ensureWorkspaceStorageFolderMeta(payload: IWorkspaceInitializationPayload): void { @@ -148,13 +146,16 @@ export class StorageService extends Disposable implements IStorageService { if (meta) { const workspaceStorageMetaPath = join(this.getWorkspaceStorageFolderPath(payload), StorageService.WORKSPACE_META_NAME); - exists(workspaceStorageMetaPath).then(exists => { - if (exists) { - return undefined; // already existing + (async function () { + try { + const storageExists = await exists(workspaceStorageMetaPath); + if (!storageExists) { + await writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2)); + } + } catch (error) { + onUnexpectedError(error); } - - return writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2)); - }).then(undefined, error => onUnexpectedError(error)); + })(); } } @@ -184,16 +185,16 @@ export class StorageService extends Disposable implements IStorageService { this.getStorage(scope).delete(key); } - close(): Promise { + async close(): Promise { // Signal as event so that clients can still store data this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN }); // Do it - return Promise.all([ + await Promise.all([ this.globalStorage.close(), this.workspaceStorage.close() - ]).then(() => undefined); + ]); } private getStorage(scope: StorageScope): IStorage { @@ -208,77 +209,75 @@ export class StorageService extends Disposable implements IStorageService { return scope === StorageScope.GLOBAL ? this.globalStorage.checkIntegrity(full) : this.workspaceStorage.checkIntegrity(full); } - logStorage(): Promise { - return Promise.all([ + async logStorage(): Promise { + const result = await Promise.all([ this.globalStorage.items, this.workspaceStorage.items, this.globalStorage.checkIntegrity(true /* full */), this.workspaceStorage.checkIntegrity(true /* full */) - ]).then(result => { - const safeParse = (value: string) => { - try { - return JSON.parse(value); - } catch (error) { - return value; - } - }; + ]); - const globalItems = new Map(); - const globalItemsParsed = new Map(); - result[0].forEach((value, key) => { - globalItems.set(key, value); - globalItemsParsed.set(key, safeParse(value)); - }); + const safeParse = (value: string) => { + try { + return JSON.parse(value); + } catch (error) { + return value; + } + }; - const workspaceItems = new Map(); - const workspaceItemsParsed = new Map(); - result[1].forEach((value, key) => { - workspaceItems.set(key, value); - workspaceItemsParsed.set(key, safeParse(value)); - }); - - console.group(`Storage: Global (integrity: ${result[2]}, path: ${this.environmentService.globalStorageHome})`); - let globalValues: { key: string, value: string }[] = []; - globalItems.forEach((value, key) => { - globalValues.push({ key, value }); - }); - console.table(globalValues); - console.groupEnd(); - - console.log(globalItemsParsed); - - console.group(`Storage: Workspace (integrity: ${result[3]}, load: ${getDuration('willInitWorkspaceStorage', 'didInitWorkspaceStorage')}, path: ${this.workspaceStoragePath})`); - let workspaceValues: { key: string, value: string }[] = []; - workspaceItems.forEach((value, key) => { - workspaceValues.push({ key, value }); - }); - console.table(workspaceValues); - console.groupEnd(); - - console.log(workspaceItemsParsed); + const globalItems = new Map(); + const globalItemsParsed = new Map(); + result[0].forEach((value, key) => { + globalItems.set(key, value); + globalItemsParsed.set(key, safeParse(value)); }); + + const workspaceItems = new Map(); + const workspaceItemsParsed = new Map(); + result[1].forEach((value, key) => { + workspaceItems.set(key, value); + workspaceItemsParsed.set(key, safeParse(value)); + }); + + console.group(`Storage: Global (integrity: ${result[2]}, path: ${this.environmentService.globalStorageHome})`); + let globalValues: { key: string, value: string }[] = []; + globalItems.forEach((value, key) => { + globalValues.push({ key, value }); + }); + console.table(globalValues); + console.groupEnd(); + + console.log(globalItemsParsed); + + console.group(`Storage: Workspace (integrity: ${result[3]}, load: ${getDuration('willInitWorkspaceStorage', 'didInitWorkspaceStorage')}, path: ${this.workspaceStoragePath})`); + let workspaceValues: { key: string, value: string }[] = []; + workspaceItems.forEach((value, key) => { + workspaceValues.push({ key, value }); + }); + console.table(workspaceValues); + console.groupEnd(); + + console.log(workspaceItemsParsed); } - migrate(toWorkspace: IWorkspaceInitializationPayload): Promise { + async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise { if (this.workspaceStoragePath === SQLiteStorageDatabase.IN_MEMORY_PATH) { return Promise.resolve(); // no migration needed if running in memory } // Close workspace DB to be able to copy - return this.workspaceStorage.close().then(() => { + await this.workspaceStorage.close(); - // Prepare new workspace storage folder - return this.prepareWorkspaceStorageFolder(toWorkspace).then(result => { - const newWorkspaceStoragePath = join(result.path, StorageService.WORKSPACE_STORAGE_NAME); + // Prepare new workspace storage folder + const result = await this.prepareWorkspaceStorageFolder(toWorkspace); - // Copy current storage over to new workspace storage - return copy(this.workspaceStoragePath, newWorkspaceStoragePath).then(() => { + const newWorkspaceStoragePath = join(result.path, StorageService.WORKSPACE_STORAGE_NAME); - // Recreate and init workspace storage - return this.createWorkspaceStorage(newWorkspaceStoragePath).init(); - }); - }); - }); + // Copy current storage over to new workspace storage + await copy(this.workspaceStoragePath, newWorkspaceStoragePath); + + // Recreate and init workspace storage + return this.createWorkspaceStorage(newWorkspaceStoragePath).init(); } } diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index 3b622447c0..098ffd5b3d 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -89,15 +89,15 @@ export class TelemetryService implements ITelemetryService { return this._userOptIn && this._enabled; } - getTelemetryInfo(): Promise { - return this._commonProperties.then(values => { - // well known properties - let sessionId = values['sessionID']; - let instanceId = values['common.instanceId']; - let machineId = values['common.machineId']; + async getTelemetryInfo(): Promise { + const values = await this._commonProperties; - return { sessionId, instanceId, machineId }; - }); + // well known properties + let sessionId = values['sessionID']; + let instanceId = values['common.instanceId']; + let machineId = values['common.machineId']; + + return { sessionId, instanceId, machineId }; } dispose(): void { diff --git a/src/vs/platform/telemetry/node/commonProperties.ts b/src/vs/platform/telemetry/node/commonProperties.ts index 5809b38d9a..6fa9d1ee28 100644 --- a/src/vs/platform/telemetry/node/commonProperties.ts +++ b/src/vs/platform/telemetry/node/commonProperties.ts @@ -10,8 +10,9 @@ import { readFile } from 'vs/base/node/pfs'; // {{SQL CARBON EDIT}} import product from 'vs/platform/product/node/product'; +const productObject = product; -export function resolveCommonProperties(commit: string | undefined, version: string | undefined, machineId: string | undefined, installSourcePath: string): Promise<{ [name: string]: string | undefined; }> { +export async function resolveCommonProperties(commit: string | undefined, version: string | undefined, machineId: string | undefined, installSourcePath: string, product?: string): Promise<{ [name: string]: string | undefined; }> { const result: { [name: string]: string | undefined; } = Object.create(null); // {{SQL CARBON EDIT}} @@ -36,8 +37,8 @@ export function resolveCommonProperties(commit: string | undefined, version: str result['common.nodeArch'] = process.arch; // __GDPR__COMMON__ "common.product" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } // {{SQL CARBON EDIT}} - result['common.product'] = product.nameShort || 'desktop'; - result['common.application.name'] = product.nameLong; + result['common.product'] = productObject.nameShort || 'desktop'; + result['common.application.name'] = productObject.nameLong; // dynamic properties which value differs on each call let seq = 0; @@ -65,13 +66,14 @@ export function resolveCommonProperties(commit: string | undefined, version: str result['common.snap'] = 'true'; } - return readFile(installSourcePath, 'utf8').then(contents => { + try { + const contents = await readFile(installSourcePath, 'utf8'); // __GDPR__COMMON__ "common.source" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['common.source'] = contents.slice(0, 30); + } catch (error) { + // ignore error + } - return result; - }, error => { - return result; - }); + return result; } diff --git a/src/vs/platform/telemetry/node/telemetryNodeUtils.ts b/src/vs/platform/telemetry/node/telemetryNodeUtils.ts index 1a5a82124d..f3f4c9fb43 100644 --- a/src/vs/platform/telemetry/node/telemetryNodeUtils.ts +++ b/src/vs/platform/telemetry/node/telemetryNodeUtils.ts @@ -3,20 +3,18 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - import { URI } from 'vs/base/common/uri'; import product from 'vs/platform/product/node/product'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -export function addGAParameters(telemetryService: ITelemetryService, environmentService: IEnvironmentService, uri: URI, origin: string, experiment = '1'): Promise { +export async function addGAParameters(telemetryService: ITelemetryService, environmentService: IEnvironmentService, uri: URI, origin: string, experiment = '1'): Promise { if (environmentService.isBuilt && !environmentService.isExtensionDevelopment && !environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { if (uri.scheme === 'https' && uri.authority === 'code.visualstudio.com') { - return telemetryService.getTelemetryInfo() - .then(info => { - return uri.with({ query: `${uri.query ? uri.query + '&' : ''}utm_source=VsCode&utm_medium=${encodeURIComponent(origin)}&utm_campaign=${encodeURIComponent(info.instanceId)}&utm_content=${encodeURIComponent(experiment)}` }); - }); + const info = await telemetryService.getTelemetryInfo(); + + return uri.with({ query: `${uri.query ? uri.query + '&' : ''}utm_source=VsCode&utm_medium=${encodeURIComponent(origin)}&utm_campaign=${encodeURIComponent(info.instanceId)}&utm_content=${encodeURIComponent(experiment)}` }); } } - return Promise.resolve(uri); + return uri; } diff --git a/src/vs/platform/telemetry/node/workbenchCommonProperties.ts b/src/vs/platform/telemetry/node/workbenchCommonProperties.ts index 6f786fc268..e92747789c 100644 --- a/src/vs/platform/telemetry/node/workbenchCommonProperties.ts +++ b/src/vs/platform/telemetry/node/workbenchCommonProperties.ts @@ -14,35 +14,38 @@ export const lastSessionDateStorageKey = 'telemetry.lastSessionDate'; // {{ SQL CARBON EDIT }} import product from 'vs/platform/product/node/product'; -export function resolveWorkbenchCommonProperties(storageService: IStorageService, commit: string | undefined, version: string | undefined, machineId: string, installSourcePath: string): Promise<{ [name: string]: string | undefined }> { - return resolveCommonProperties(commit, version, machineId, installSourcePath).then(result => { - const instanceId = storageService.get(instanceStorageKey, StorageScope.GLOBAL)!; - const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.GLOBAL)!; - const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL)!; +export async function resolveWorkbenchCommonProperties(storageService: IStorageService, commit: string | undefined, version: string | undefined, machineId: string, installSourcePath: string): Promise<{ [name: string]: string | undefined }> { + const result = await resolveCommonProperties(commit, version, machineId, installSourcePath); + const instanceId = storageService.get(instanceStorageKey, StorageScope.GLOBAL)!; + const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.GLOBAL)!; + const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL)!; - // __GDPR__COMMON__ "common.version.shell" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - result['common.version.shell'] = process.versions && process.versions['electron']; - // __GDPR__COMMON__ "common.version.renderer" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } - result['common.version.renderer'] = process.versions && process.versions['chrome']; - // {{SQL CARBON EDIT}} - result['common.application.name'] = product.nameLong; - // {{SQL CARBON EDIT}} - result['common.userId'] = ''; + // __GDPR__COMMON__ "common.version.shell" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + // result['common.version.shell'] = process.versions && process.versions['electron']; + // __GDPR__COMMON__ "common.version.renderer" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + // result['common.version.renderer'] = process.versions && process.versions['chrome']; + // __GDPR__COMMON__ "common.firstSessionDate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + // result['common.firstSessionDate'] = firstSessionDate; + // __GDPR__COMMON__ "common.lastSessionDate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + // result['common.lastSessionDate'] = lastSessionDate || ''; + // __GDPR__COMMON__ "common.isNewSession" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + // result['common.isNewSession'] = !lastSessionDate ? '1' : '0'; + // __GDPR__COMMON__ "common.instanceId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + // result['common.instanceId'] = instanceId; - // {{SQL CARBON EDIT}} - // // __GDPR__COMMON__ "common.firstSessionDate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - // result['common.firstSessionDate'] = firstSessionDate; - // // __GDPR__COMMON__ "common.lastSessionDate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - // result['common.lastSessionDate'] = lastSessionDate || ''; - // // __GDPR__COMMON__ "common.isNewSession" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - // result['common.isNewSession'] = !lastSessionDate ? '1' : '0'; - // // __GDPR__COMMON__ "common.instanceId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - // result['common.instanceId'] = instanceId; + // __GDPR__COMMON__ "common.version.shell" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + result['common.version.shell'] = process.versions && process.versions['electron']; + // __GDPR__COMMON__ "common.version.renderer" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + result['common.version.renderer'] = process.versions && process.versions['chrome']; + // {{SQL CARBON EDIT}} + result['common.application.name'] = product.nameLong; + // {{SQL CARBON EDIT}} + result['common.userId'] = ''; - // {{SQL CARBON EDIT}} - setUsageDates(storageService); - return result; - }); + // {{SQL CARBON EDIT}} + setUsageDates(storageService); + + return result; } // {{SQL CARBON EDIT}} diff --git a/src/vs/platform/windows/electron-browser/windowsService.ts b/src/vs/platform/windows/electron-browser/windowsService.ts index e7cc17dcf1..631fc87334 100644 --- a/src/vs/platform/windows/electron-browser/windowsService.ts +++ b/src/vs/platform/windows/electron-browser/windowsService.ts @@ -74,13 +74,13 @@ export class WindowsService implements IWindowsService { return this.channel.call('closeWorkspace', windowId); } - enterWorkspace(windowId: number, path: URI): Promise { - return this.channel.call('enterWorkspace', [windowId, path]).then((result: IEnterWorkspaceResult) => { - if (result) { - result.workspace = reviveWorkspaceIdentifier(result.workspace); - } - return result; - }); + async enterWorkspace(windowId: number, path: URI): Promise { + const result: IEnterWorkspaceResult = await this.channel.call('enterWorkspace', [windowId, path]); + if (result) { + result.workspace = reviveWorkspaceIdentifier(result.workspace); + } + + return result; } toggleFullScreen(windowId: number): Promise { @@ -103,13 +103,12 @@ export class WindowsService implements IWindowsService { return this.channel.call('clearRecentlyOpened'); } - getRecentlyOpened(windowId: number): Promise { - return this.channel.call('getRecentlyOpened', windowId) - .then((recentlyOpened: IRecentlyOpened) => { - recentlyOpened.workspaces.forEach(recent => isRecentWorkspace(recent) ? recent.workspace = reviveWorkspaceIdentifier(recent.workspace) : recent.folderUri = URI.revive(recent.folderUri)); - recentlyOpened.files.forEach(recent => recent.fileUri = URI.revive(recent.fileUri)); - return recentlyOpened; - }); + async getRecentlyOpened(windowId: number): Promise { + const recentlyOpened: IRecentlyOpened = await this.channel.call('getRecentlyOpened', windowId); + recentlyOpened.workspaces.forEach(recent => isRecentWorkspace(recent) ? recent.workspace = reviveWorkspaceIdentifier(recent.workspace) : recent.folderUri = URI.revive(recent.folderUri)); + recentlyOpened.files.forEach(recent => recent.fileUri = URI.revive(recent.fileUri)); + + return recentlyOpened; } newWindowTab(): Promise { @@ -196,18 +195,26 @@ export class WindowsService implements IWindowsService { return this.channel.call('openNewWindow', options); } - getWindows(): Promise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]> { - return this.channel.call<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]>('getWindows').then(result => { - for (const win of result) { - if (win.folderUri) { - win.folderUri = URI.revive(win.folderUri); - } - if (win.workspace) { - win.workspace = reviveWorkspaceIdentifier(win.workspace); - } + async getWindows(): Promise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]> { + const result = await this.channel.call<{ + id: number; + workspace?: IWorkspaceIdentifier; + folderUri?: ISingleFolderWorkspaceIdentifier; + title: string; + filename?: string; + }[]>('getWindows'); + + for (const win of result) { + if (win.folderUri) { + win.folderUri = URI.revive(win.folderUri); } - return result; - }); + + if (win.workspace) { + win.workspace = reviveWorkspaceIdentifier(win.workspace); + } + } + + return result; } getWindowCount(): Promise { diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index 1e4af393ec..cc1c407578 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -103,13 +103,14 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain return isEqualOrParent(path, this.environmentService.untitledWorkspacesHome); } - createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { + async createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority); const configPath = workspace.configPath.fsPath; - return mkdirp(dirname(configPath)).then(() => { - return writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')).then(() => workspace); - }); + await mkdirp(dirname(configPath)); + await writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t')); + + return workspace; } createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): IWorkspaceIdentifier { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index ce42128cf1..6783daf65e 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -5763,8 +5763,11 @@ declare module 'vscode' { * to allow using a static localhost port inside the webview that is resolved to random port that a service is * running on. * - * If a webview accesses localhost content, we recomend that you specify port mappings even if + * If a webview accesses localhost content, we recommend that you specify port mappings even if * the `webviewPort` and `extensionHostPort` ports are the same. + * + * *Note* that port mappings only work for `http` or `https` urls. Websocket urls (e.g. `ws://localhost:3000`) + * cannot be mapped to another port. */ readonly portMapping?: ReadonlyArray; } @@ -8940,6 +8943,237 @@ declare module 'vscode' { */ export const onDidChange: Event; } + + //#region Comments + + /** + * Collapsible state of a [comment thread](#CommentThread) + */ + export enum CommentThreadCollapsibleState { + /** + * Determines an item is collapsed + */ + Collapsed = 0, + + /** + * Determines an item is expanded + */ + Expanded = 1 + } + + /** + * Comment mode of a [comment](#Comment) + */ + export enum CommentMode { + /** + * Displays the comment editor + */ + Editing = 0, + + /** + * Displays the preview of the comment + */ + Preview = 1 + } + + /** + * A collection of [comments](#Comment) representing a conversation at a particular range in a document. + */ + export interface CommentThread { + /** + * The uri of the document the thread has been created on. + */ + readonly resource: Uri; + + /** + * The range the comment thread is located within the document. The thread icon will be shown + * at the first line of the range. + */ + range: Range; + + /** + * The ordered comments of the thread. + */ + comments: ReadonlyArray; + + /** + * Whether the thread should be collapsed or expanded when opening the document. + * Defaults to Collapsed. + */ + collapsibleState: CommentThreadCollapsibleState; + + /** + * Context value of the comment thread. This can be used to contribute thread specific actions. + * For example, a comment thread is given a context value as `editable`. When contributing actions to `comments/commentThread/title` + * using `menus` extension point, you can specify context value for key `commentThread` in `when` expression like `commentThread == editable`. + * ``` + * "contributes": { + * "menus": { + * "comments/commentThread/title": [ + * { + * "command": "extension.deleteCommentThread", + * "when": "commentThread == editable" + * } + * ] + * } + * } + * ``` + * This will show action `extension.deleteCommentThread` only for comment threads with `contextValue` is `editable`. + */ + contextValue?: string; + + /** + * The optional human-readable label describing the [Comment Thread](#CommentThread) + */ + label?: string; + + /** + * Dispose this comment thread. + * + * Once disposed, this comment thread will be removed from visible editors and Comment Panel when approriate. + */ + dispose(): void; + } + + /** + * Author information of a [comment](#Comment) + */ + export interface CommentAuthorInformation { + /** + * The display name of the author of the comment + */ + name: string; + + /** + * The optional icon path for the author + */ + iconPath?: Uri; + } + + /** + * A comment is displayed within the editor or the Comments Panel, depending on how it is provided. + */ + export interface Comment { + /** + * The human-readable comment body + */ + body: string | MarkdownString; + + /** + * [Comment mode](#CommentMode) of the comment + */ + mode: CommentMode; + + /** + * The [author information](#CommentAuthorInformation) of the comment + */ + author: CommentAuthorInformation; + + /** + * Context value of the comment. This can be used to contribute comment specific actions. + * For example, a comment is given a context value as `editable`. When contributing actions to `comments/comment/title` + * using `menus` extension point, you can specify context value for key `comment` in `when` expression like `comment == editable`. + * ``` + * "contributes": { + * "menus": { + * "comments/comment/title": [ + * { + * "command": "extension.deleteComment", + * "when": "comment == editable" + * } + * ] + * } + * } + * ``` + * This will show action `extension.deleteComment` only for comments with `contextValue` is `editable`. + */ + contextValue?: string; + + /** + * Optional label describing the [Comment](#Comment) + * Label will be rendered next to authorName if exists. + */ + label?: string; + } + + /** + * Command argument for actions registered in `comments/commentThread/actions`. + */ + export interface CommentReply { + /** + * The active [comment thread](#CommentThread) + */ + thread: CommentThread; + + /** + * The value in the comment editor + */ + text: string; + } + + /** + * Commenting range provider for a [comment controller](#CommentController). + */ + export interface CommentingRangeProvider { + /** + * Provide a list of ranges which allow new comment threads creation or null for a given document + */ + provideCommentingRanges(document: TextDocument, token: CancellationToken): ProviderResult; + } + + /** + * A comment controller is able to provide [comments](#CommentThread) support to the editor and + * provide users various ways to interact with comments. + */ + export interface CommentController { + /** + * The id of this comment controller. + */ + readonly id: string; + + /** + * The human-readable label of this comment controller. + */ + readonly label: string; + + /** + * Optional commenting range provider. Provide a list [ranges](#Range) which support commenting to any given resource uri. + * + * If not provided, users can leave comments in any document opened in the editor. + */ + commentingRangeProvider?: CommentingRangeProvider; + + /** + * Create a [comment thread](#CommentThread). The comment thread will be displayed in visible text editors (if the resource matches) + * and Comments Panel once created. + * + * @param resource The uri of the document the thread has been created on. + * @param range The range the comment thread is located within the document. + * @param comments The ordered comments of the thread. + */ + createCommentThread(uri: Uri, range: Range, comments: Comment[]): CommentThread; + + /** + * Dispose this comment controller. + * + * Once disposed, all [comment threads](#CommentThread) created by this comment controller will also be removed from the editor + * and Comments Panel. + */ + dispose(): void; + } + + namespace comments { + /** + * Creates a new [comment controller](#CommentController) instance. + * + * @param id An `id` for the comment controller. + * @param label A human-readable string for the comment controller. + * @return An instance of [comment controller](#CommentController). + */ + export function createCommentController(id: string, label: string): CommentController; + } + + //#endregion } /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 65e7223889..7ef9a092db 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -957,15 +957,6 @@ declare module 'vscode' { reactionProvider?: CommentReactionProvider; } - export interface CommentController { - /** - * The active [comment thread](#CommentThread) or `undefined`. The `activeCommentThread` is the comment thread of - * the comment widget that currently has focus. It's `undefined` when the focus is not in any comment thread widget, or - * the comment widget created from [comment thread template](#CommentThreadTemplate). - */ - readonly activeCommentThread: CommentThread | undefined; - } - namespace workspace { /** * DEPRECATED @@ -979,21 +970,6 @@ declare module 'vscode' { export function registerWorkspaceCommentProvider(provider: WorkspaceCommentProvider): Disposable; } - /** - * Collapsible state of a [comment thread](#CommentThread) - */ - export enum CommentThreadCollapsibleState { - /** - * Determines an item is collapsed - */ - Collapsed = 0, - - /** - * Determines an item is expanded - */ - Expanded = 1 - } - /** * A collection of [comments](#Comment) representing a conversation at a particular range in a document. */ @@ -1008,28 +984,6 @@ declare module 'vscode' { */ readonly uri: Uri; - /** - * The range the comment thread is located within the document. The thread icon will be shown - * at the first line of the range. - */ - readonly range: Range; - - /** - * The ordered comments of the thread. - */ - comments: Comment[]; - - /** - * Whether the thread should be collapsed or expanded when opening the document. - * Defaults to Collapsed. - */ - collapsibleState: CommentThreadCollapsibleState; - - /** - * The optional human-readable label describing the [Comment Thread](#CommentThread) - */ - label?: string; - /** * Optional accept input command * @@ -1038,46 +992,6 @@ declare module 'vscode' { * This command will disabled when the comment editor is empty. */ acceptInputCommand?: Command; - - - /** - * Dispose this comment thread. - * - * Once disposed, this comment thread will be removed from visible editors and Comment Panel when approriate. - */ - dispose(): void; - } - - /** - * Author information of a [comment](#Comment) - */ - - export interface CommentAuthorInformation { - /** - * The display name of the author of the comment - */ - name: string; - - /** - * The optional icon path for the author - */ - iconPath?: Uri; - } - - /** - * Author information of a [comment](#Comment) - */ - - export interface CommentAuthorInformation { - /** - * The display name of the author of the comment - */ - name: string; - - /** - * The optional icon path for the author - */ - iconPath?: Uri; } /** @@ -1089,22 +1003,6 @@ declare module 'vscode' { */ id: string; - /** - * The human-readable comment body - */ - body: MarkdownString; - - /** - * The author information of the comment - */ - author: CommentAuthorInformation; - - /** - * Optional label describing the [Comment](#Comment) - * Label will be rendered next to authorName if exists. - */ - label?: string; - /** * The command to be executed if the comment is selected in the Comments Panel */ @@ -1124,16 +1022,6 @@ declare module 'vscode' { * Setter and getter for the contents of the comment input box */ value: string; - - /** - * The uri of the document comment input box has been created on - */ - resource: Uri; - - /** - * The range the comment input box is located within the document - */ - range: Range; } /** @@ -1146,36 +1034,12 @@ declare module 'vscode' { provideCommentingRanges(document: TextDocument, token: CancellationToken): ProviderResult; } - /** - * Comment thread template for new comment thread creation. - */ - export interface CommentThreadTemplate { + export interface EmptyCommentThreadFactory { /** - * The human-readable label describing the [Comment Thread](#CommentThread) + * The method `createEmptyCommentThread` is called when users attempt to create new comment thread from the gutter or command palette. + * Extensions still need to call `createCommentThread` inside this call when appropriate. */ - readonly label: string; - - /** - * Optional accept input command - * - * `acceptInputCommand` is the default action rendered on Comment Widget, which is always placed rightmost. - * This command will be invoked when users the user accepts the value in the comment editor. - * This command will disabled when the comment editor is empty. - */ - readonly acceptInputCommand?: Command; - - /** - * Optional additonal commands. - * - * `additionalCommands` are the secondary actions rendered on Comment Widget. - */ - readonly additionalCommands?: Command[]; - - /** - * The command to be executed when users try to delete the comment thread. Currently, this is only called - * when the user collapses a comment thread that has no comments in it. - */ - readonly deleteCommand?: Command; + createEmptyCommentThread(document: TextDocument, range: Range): ProviderResult; } /** @@ -1183,41 +1047,12 @@ declare module 'vscode' { * provide users various ways to interact with comments. */ export interface CommentController { - /** - * The id of this comment controller. - */ - readonly id: string; - - /** - * The human-readable label of this comment controller. - */ - readonly label: string; /** * The active [comment input box](#CommentInputBox) or `undefined`. The active `inputBox` is the input box of * the comment thread widget that currently has focus. It's `undefined` when the focus is not in any CommentInputBox. */ - readonly inputBox: CommentInputBox | undefined; - - /** - * Optional comment thread template information. - * - * The comment controller will use this information to create the comment widget when users attempt to create new comment thread - * from the gutter or command palette. - * - * When users run `CommentThreadTemplate.acceptInputCommand` or `CommentThreadTemplate.additionalCommands`, extensions should create - * the approriate [CommentThread](#CommentThread). - * - * If not provided, users won't be able to create new comment threads in the editor. - */ - template?: CommentThreadTemplate; - - /** - * Optional commenting range provider. Provide a list [ranges](#Range) which support commenting to any given resource uri. - * - * If not provided and `emptyCommentThreadFactory` exits, users can leave comments in any document opened in the editor. - */ - commentingRangeProvider?: CommentingRangeProvider; + readonly inputBox?: CommentInputBox; /** * Create a [comment thread](#CommentThread). The comment thread will be displayed in visible text editors (if the resource matches) @@ -1230,6 +1065,16 @@ declare module 'vscode' { */ createCommentThread(id: string, uri: Uri, range: Range, comments: Comment[]): CommentThread; + /** + * Optional new comment thread factory. + */ + emptyCommentThreadFactory?: EmptyCommentThreadFactory; + + /** + * Optional reaction provider + */ + reactionProvider?: CommentReactionProvider; + /** * Dispose this comment controller. * diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 861929630b..76b910bcd1 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -106,6 +106,16 @@ export class MainThreadCommentThread implements modes.CommentThread2 { this._onDidChangeLabel.fire(this._label); } + private _contextValue: string | undefined; + + get contextValue(): string | undefined { + return this._contextValue; + } + + set contextValue(context: string | undefined) { + this._contextValue = context; + } + private _onDidChangeLabel = new Emitter(); get onDidChangeLabel(): Event { return this._onDidChangeLabel.event; } @@ -204,6 +214,7 @@ export class MainThreadCommentThread implements modes.CommentThread2 { batchUpdate( range: IRange, label: string, + contextValue: string | undefined, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], @@ -211,6 +222,7 @@ export class MainThreadCommentThread implements modes.CommentThread2 { collapsibleState: modes.CommentThreadCollapsibleState) { this._range = range; this._label = label; + this._contextValue = contextValue; this._comments = comments; this._acceptInputCommand = acceptInputCommand; this._additionalCommands = additionalCommands; @@ -247,6 +259,10 @@ export class MainThreadCommentController { return this._id; } + get contextValue(): string { + return this._id; + } + get proxy(): ExtHostCommentsShape { return this._proxy; } @@ -319,13 +335,14 @@ export class MainThreadCommentController { resource: UriComponents, range: IRange, label: string, + contextValue: string | undefined, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], deleteCommand: modes.Command | undefined, collapsibleState: modes.CommentThreadCollapsibleState): void { let thread = this.getKnownThread(commentThreadHandle); - thread.batchUpdate(range, label, comments, acceptInputCommand, additionalCommands, deleteCommand, collapsibleState); + thread.batchUpdate(range, label, contextValue, comments, acceptInputCommand, additionalCommands, deleteCommand, collapsibleState); this._commentService.updateComments(this._uniqueId, { added: [], @@ -349,6 +366,14 @@ export class MainThreadCommentController { thread.dispose(); } + deleteCommentThreadMain(commentThreadId: string) { + this._threads.forEach(thread => { + if (thread.threadId === commentThreadId) { + this._proxy.$deleteCommentThread(this._handle, thread.commentThreadHandle); + } + }); + } + updateInput(input: string) { let thread = this.activeCommentThread; @@ -377,31 +402,26 @@ export class MainThreadCommentController { } let commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token); + let staticContribution = await this._proxy.$checkStaticContribution(this.handle); return { owner: this._uniqueId, label: this.label, threads: ret, - commentingRanges: commentingRanges ? - { - resource: resource, ranges: commentingRanges, newCommentThreadCallback: async (uri: UriComponents, range: IRange) => { - let threadHandle = await this._proxy.$createNewCommentWidgetCallback(this.handle, uri, range, token); + commentingRanges: commentingRanges ? { + resource: resource, + ranges: commentingRanges, + newCommentThreadCallback: staticContribution ? undefined : async (uri: UriComponents, range: IRange) => { + let threadHandle = await this._proxy.$createNewCommentWidgetCallback(this.handle, uri, range, token); - // if (threadHandle !== undefined) { {{SQL CARBON EDIT}} @anthonydresser this never happens but throws error because of strict null checks - // return this.getKnownThread(threadHandle); - // } + // if (threadHandle !== undefined) { {{SQL CARBON EDIT}} @anthonydresser this never happens but throws error because of strict null checks + // return this.getKnownThread(threadHandle); + // } - return undefined; // {{SQL CARBON EDIT}} @anthonydresser revert back after strict-null-check - } - } : [], - draftMode: modes.DraftMode.NotSupported, - template: this._features.commentThreadTemplate ? { - controllerHandle: this.handle, - label: this._features.commentThreadTemplate.label, - acceptInputCommand: this._features.commentThreadTemplate.acceptInputCommand, - additionalCommands: this._features.commentThreadTemplate.additionalCommands, - deleteCommand: this._features.commentThreadTemplate.deleteCommand - } : undefined + return undefined; // {{SQL CARBON EDIT}} @anthonydresser revert back after strict-null-check + } + } : [], + draftMode: modes.DraftMode.NotSupported }; } @@ -427,26 +447,8 @@ export class MainThreadCommentController { return ret; } - getCommentThreadFromTemplate(resource: UriComponents, range: IRange): MainThreadCommentThread { - let thread = new MainThreadCommentThread( - -1, - this.handle, - '', - '', - URI.revive(resource).toString(), - range - ); - - let template = this._features.commentThreadTemplate; - - if (template) { - thread.acceptInputCommand = template.acceptInputCommand; - thread.additionalCommands = template.additionalCommands; - thread.deleteCommand = template.deleteCommand; - thread.label = template.label; - } - - return thread; + createCommentThreadTemplate(resource: UriComponents, range: IRange): void { + this._proxy.$createCommentThreadTemplate(this.handle, resource, range); } toJSON(): any { @@ -467,8 +469,6 @@ export class MainThreadComments extends Disposable implements MainThreadComments private _handlers = new Map(); private _commentControllers = new Map(); - private _activeCommentThread?: MainThreadCommentThread; - private _input?: modes.CommentInput; private _openPanelListener: IDisposable | null; constructor( @@ -477,32 +477,12 @@ export class MainThreadComments extends Disposable implements MainThreadComments @ICommentService private readonly _commentService: ICommentService, @IPanelService private readonly _panelService: IPanelService, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @IConfigurationService private readonly _configurationService: IConfigurationService + @IConfigurationService private readonly _configurationService: IConfigurationService, ) { super(); this._disposables = []; this._activeCommentThreadDisposables = []; this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostComments); - this._disposables.push(this._commentService.onDidChangeActiveCommentThread(async thread => { - let handle = (thread as MainThreadCommentThread).controllerHandle; - let controller = this._commentControllers.get(handle); - - if (!controller) { - return; - } - - this._activeCommentThreadDisposables = dispose(this._activeCommentThreadDisposables); - this._activeCommentThread = thread as MainThreadCommentThread; - controller.activeCommentThread = this._activeCommentThread; - - this._activeCommentThreadDisposables.push(this._activeCommentThread.onDidChangeInput(input => { // todo, dispose - this._input = input; - this._proxy.$onCommentWidgetInputChange(handle, URI.parse(this._activeCommentThread!.resource), this._activeCommentThread!.range, this._input ? this._input.value : undefined); - })); - - await this._proxy.$onActiveCommentThreadChange(controller.handle, controller.activeCommentThread.commentThreadHandle); - await this._proxy.$onCommentWidgetInputChange(controller.handle, URI.parse(this._activeCommentThread!.resource), this._activeCommentThread.range, this._input ? this._input.value : undefined); - })); } $registerCommentController(handle: number, id: string, label: string): void { @@ -562,6 +542,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments resource: UriComponents, range: IRange, label: string, + contextValue: string | undefined, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], @@ -573,7 +554,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments return undefined; } - return provider.updateCommentThread(commentThreadHandle, threadId, resource, range, label, comments, acceptInputCommand, additionalCommands, deleteCommand, collapsibleState); + return provider.updateCommentThread(commentThreadHandle, threadId, resource, range, label, contextValue, comments, acceptInputCommand, additionalCommands, deleteCommand, collapsibleState); } $deleteCommentThread(handle: number, commentThreadHandle: number) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 004a0b4263..91547b6982 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -141,7 +141,7 @@ export interface MainThreadCommentsShape extends IDisposable { $unregisterCommentController(handle: number): void; $updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void; $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange): modes.CommentThread2 | undefined; - $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange, label: string, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], deleteCommand: modes.Command | undefined, collapseState: modes.CommentThreadCollapsibleState): void; + $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange, label: string, contextValue: string | undefined, comments: modes.Comment[], acceptInputCommand: modes.Command | undefined, additionalCommands: modes.Command[], deleteCommand: modes.Command | undefined, collapseState: modes.CommentThreadCollapsibleState): void; $deleteCommentThread(handle: number, commentThreadHandle: number): void; $setInputValue(handle: number, input: string): void; $registerDocumentCommentProvider(handle: number, features: CommentProviderFeatures): void; @@ -1211,9 +1211,11 @@ export interface ExtHostProgressShape { export interface ExtHostCommentsShape { $provideDocumentComments(handle: number, document: UriComponents): Promise; $createNewCommentThread(handle: number, document: UriComponents, range: IRange, text: string): Promise; + $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange): void; $onCommentWidgetInputChange(commentControllerHandle: number, document: UriComponents, range: IRange, input: string | undefined): Promise; - $onActiveCommentThreadChange(commentControllerHandle: number, threadHandle: number | undefined): Promise; + $deleteCommentThread(commentControllerHandle: number, commentThreadHandle: number): void; $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise; + $checkStaticContribution(commentControllerHandle: number): Promise; $provideReactionGroup(commentControllerHandle: number): Promise; $toggleReaction(commentControllerHandle: number, threadHandle: number, uri: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise; $createNewCommentWidgetCallback(commentControllerHandle: number, uriComponents: UriComponents, range: IRange, token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index 466c45f5e2..67e6e3c4fd 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -69,6 +69,70 @@ export class ExtHostComments implements ExtHostCommentsShape { } return commentThread; + } else if (arg && arg.$mid === 8) { + const commentController = this._commentControllers.get(arg.thread.commentControlHandle); + + if (!commentController) { + return arg; + } + + const commentThread = commentController.getCommentThread(arg.thread.commentThreadHandle); + + if (!commentThread) { + return arg; + } + + return { + thread: commentThread, + text: arg.text + }; + } else if (arg && arg.$mid === 9) { + const commentController = this._commentControllers.get(arg.thread.commentControlHandle); + + if (!commentController) { + return arg; + } + + const commentThread = commentController.getCommentThread(arg.thread.commentThreadHandle); + + if (!commentThread) { + return arg; + } + + let commentUniqueId = arg.commentUniqueId; + + let comment = commentThread.getCommentByUniqueId(commentUniqueId); + + if (!comment) { + return arg; + } + + return comment; + + } else if (arg && arg.$mid === 10) { + const commentController = this._commentControllers.get(arg.thread.commentControlHandle); + + if (!commentController) { + return arg; + } + + const commentThread = commentController.getCommentThread(arg.thread.commentThreadHandle); + + if (!commentThread) { + return arg; + } + + let body = arg.text; + let commentUniqueId = arg.commentUniqueId; + + let comment = commentThread.getCommentByUniqueId(commentUniqueId); + + if (!comment) { + return arg; + } + + comment.body = body; + return comment; } return arg; @@ -88,6 +152,16 @@ export class ExtHostComments implements ExtHostCommentsShape { return commentController; } + $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange): void { + const commentController = this._commentControllers.get(commentControllerHandle); + + if (!commentController) { + return; + } + + commentController.$createCommentThreadTemplate(uriComponents, range); + } + $onCommentWidgetInputChange(commentControllerHandle: number, uriComponents: UriComponents, range: IRange, input: string): Promise { const commentController = this._commentControllers.get(commentControllerHandle); @@ -99,15 +173,12 @@ export class ExtHostComments implements ExtHostCommentsShape { return Promise.resolve(commentControllerHandle); } - $onActiveCommentThreadChange(commentControllerHandle: number, threadHandle: number): Promise { + $deleteCommentThread(commentControllerHandle: number, commentThreadHandle: number) { const commentController = this._commentControllers.get(commentControllerHandle); - if (!commentController) { - return Promise.resolve(undefined); + if (commentController) { + commentController.$deleteCommentThread(commentThreadHandle); } - - commentController.$onActiveCommentThreadChange(threadHandle); - return Promise.resolve(threadHandle); } $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise { @@ -164,7 +235,7 @@ export class ExtHostComments implements ExtHostCommentsShape { return Promise.resolve(); } - if (!(commentController as any).emptyCommentThreadFactory && !(commentController.commentingRangeProvider && commentController.commentingRangeProvider.createEmptyCommentThread)) { + if (!(commentController as any).emptyCommentThreadFactory) { return Promise.resolve(); } @@ -173,13 +244,23 @@ export class ExtHostComments implements ExtHostCommentsShape { if ((commentController as any).emptyCommentThreadFactory) { return (commentController as any).emptyCommentThreadFactory!.createEmptyCommentThread(document, extHostTypeConverter.Range.to(range)); } - - if (commentController.commentingRangeProvider && commentController.commentingRangeProvider.createEmptyCommentThread) { - return commentController.commentingRangeProvider.createEmptyCommentThread(document, extHostTypeConverter.Range.to(range)); - } }).then(() => Promise.resolve()); } + $checkStaticContribution(commentControllerHandle: number): Promise { + const commentController = this._commentControllers.get(commentControllerHandle); + + if (!commentController) { + return Promise.resolve(false); + } + + if (!(commentController as any).emptyCommentThreadFactory) { + return Promise.resolve(true); + } + + return Promise.resolve(false); + } + registerWorkspaceCommentProvider( extensionId: ExtensionIdentifier, provider: vscode.WorkspaceCommentProvider @@ -376,12 +457,18 @@ export class ExtHostComments implements ExtHostCommentsShape { export class ExtHostCommentThread implements vscode.CommentThread { private static _handlePool: number = 0; readonly handle = ExtHostCommentThread._handlePool++; + public commentHandle: number = 0; + + set threadId(id: string) { + this._id = id; + } + get threadId(): string { - return this._id; + return this._id!; } get id(): string { - return this._id; + return this._id!; } get resource(): vscode.Uri { @@ -417,6 +504,17 @@ export class ExtHostCommentThread implements vscode.CommentThread { this._onDidUpdateCommentThread.fire(); } + private _contextValue: string | undefined; + + get contextValue(): string | undefined { + return this._contextValue; + } + + set contextValue(context: string | undefined) { + this._contextValue = context; + this._onDidUpdateCommentThread.fire(); + } + get comments(): vscode.Comment[] { return this._comments; } @@ -475,15 +573,21 @@ export class ExtHostCommentThread implements vscode.CommentThread { return this._isDiposed; } + private _commentsMap: Map = new Map(); + constructor( private _proxy: MainThreadCommentsShape, private readonly _commandsConverter: CommandsConverter, private _commentController: ExtHostCommentController, - private _id: string, + private _id: string | undefined, private _uri: vscode.Uri, private _range: vscode.Range, private _comments: vscode.Comment[] ) { + if (this._id === undefined) { + this._id = `${_commentController.id}.${this.handle}`; + } + this._proxy.$createCommentThread( this._commentController.handle, this.handle, @@ -507,7 +611,8 @@ export class ExtHostCommentThread implements vscode.CommentThread { eventuallyUpdateCommentThread(): void { const commentThreadRange = extHostTypeConverter.Range.from(this._range); const label = this.label; - const comments = this._comments.map(cmt => { return convertToModeComment(this._commentController, cmt, this._commandsConverter); }); + const contextValue = this.contextValue; + const comments = this._comments.map(cmt => { return convertToModeComment2(this, this._commentController, cmt, this._commandsConverter, this._commentsMap); }); const acceptInputCommand = this._acceptInputCommand ? this._commandsConverter.toInternal(this._acceptInputCommand) : undefined; const additionalCommands = this._additionalCommands ? this._additionalCommands.map(x => this._commandsConverter.toInternal(x)) : []; const deleteCommand = this._deleteCommand ? this._commandsConverter.toInternal(this._deleteCommand) : undefined; @@ -516,10 +621,11 @@ export class ExtHostCommentThread implements vscode.CommentThread { this._proxy.$updateCommentThread( this._commentController.handle, this.handle, - this._id, + this._id!, this._uri, commentThreadRange, label, + contextValue, comments, acceptInputCommand, additionalCommands, @@ -538,6 +644,18 @@ export class ExtHostCommentThread implements vscode.CommentThread { return undefined; } + getCommentByUniqueId(uniqueId: number): vscode.Comment | undefined { + for (let key of this._commentsMap) { + let comment = key[0]; + let id = key[1]; + if (uniqueId === id) { + return comment; + } + } + + return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check + } + dispose() { this._localDisposables.forEach(disposable => disposable.dispose()); this._proxy.$deleteCommentThread( @@ -599,15 +717,6 @@ class ExtHostCommentController implements vscode.CommentController { } public inputBox: ExtHostCommentInputBox | undefined; - private _activeCommentThread: ExtHostCommentThread | undefined; - - public get activeCommentThread(): ExtHostCommentThread | undefined { - if (this._activeCommentThread && this._activeCommentThread.isDisposed) { - this._activeCommentThread = undefined; - } - - return this._activeCommentThread; - } public activeCommentingRange?: vscode.Range; @@ -618,30 +727,6 @@ class ExtHostCommentController implements vscode.CommentController { private _threads: Map = new Map(); commentingRangeProvider?: vscode.CommentingRangeProvider & { createEmptyCommentThread: (document: vscode.TextDocument, range: types.Range) => Promise; }; - private _template: vscode.CommentThreadTemplate | undefined; - - get template(): vscode.CommentThreadTemplate | undefined { - return this._template; - } - - set template(newTemplate: vscode.CommentThreadTemplate | undefined) { - this._template = newTemplate; - - if (newTemplate) { - const acceptInputCommand = newTemplate.acceptInputCommand ? this._commandsConverter.toInternal(newTemplate.acceptInputCommand) : undefined; - const additionalCommands = newTemplate.additionalCommands ? newTemplate.additionalCommands.map(x => this._commandsConverter.toInternal(x)) : []; - const deleteCommand = newTemplate.deleteCommand ? this._commandsConverter.toInternal(newTemplate.deleteCommand) : undefined; - this._proxy.$updateCommentControllerFeatures(this.handle, { - commentThreadTemplate: { - label: newTemplate.label, - acceptInputCommand, - additionalCommands, - deleteCommand - } - }); - } - } - private _commentReactionProvider?: vscode.CommentReactionProvider; get reactionProvider(): vscode.CommentReactionProvider | undefined { @@ -666,12 +751,37 @@ class ExtHostCommentController implements vscode.CommentController { this._proxy.$registerCommentController(this.handle, _id, _label); } - createCommentThread(id: string, resource: vscode.Uri, range: vscode.Range, comments: vscode.Comment[]): vscode.CommentThread { - const commentThread = new ExtHostCommentThread(this._proxy, this._commandsConverter, this, id, resource, range, comments); + createCommentThread(resource: vscode.Uri, range: vscode.Range, comments: vscode.Comment[]): vscode.CommentThread; + createCommentThread(id: string, resource: vscode.Uri, range: vscode.Range, comments: vscode.Comment[]): vscode.CommentThread; + createCommentThread(arg0: vscode.Uri | string, arg1: vscode.Uri | vscode.Range, arg2: vscode.Range | vscode.Comment[], arg3?: vscode.Comment[]): vscode.CommentThread { + if (typeof arg0 === 'string') { + const commentThread = new ExtHostCommentThread(this._proxy, this._commandsConverter, this, arg0, arg1 as vscode.Uri, arg2 as vscode.Range, arg3 as vscode.Comment[]); + this._threads.set(commentThread.handle, commentThread); + return commentThread; + } else { + const commentThread = new ExtHostCommentThread(this._proxy, this._commandsConverter, this, undefined, arg0 as vscode.Uri, arg1 as vscode.Range, arg2 as vscode.Comment[]); + this._threads.set(commentThread.handle, commentThread); + return commentThread; + } + } + + $createCommentThreadTemplate(uriComponents: UriComponents, range: IRange) { + const commentThread = new ExtHostCommentThread(this._proxy, this._commandsConverter, this, undefined, URI.revive(uriComponents), extHostTypeConverter.Range.to(range), []); + commentThread.collapsibleState = modes.CommentThreadCollapsibleState.Expanded; this._threads.set(commentThread.handle, commentThread); return commentThread; } + $deleteCommentThread(threadHandle: number) { + let thread = this._threads.get(threadHandle); + + if (thread) { + thread.dispose(); + } + + this._threads.delete(threadHandle); + } + $onCommentWidgetInputChange(uriComponents: UriComponents, range: IRange, input: string) { if (!this.inputBox) { this.inputBox = new ExtHostCommentInputBox(this._proxy, this.handle, URI.revive(uriComponents), extHostTypeConverter.Range.to(range), input); @@ -680,10 +790,6 @@ class ExtHostCommentController implements vscode.CommentController { } } - $onActiveCommentThreadChange(threadHandle: number) { - this._activeCommentThread = this.getCommentThread(threadHandle); - } - getCommentThread(handle: number) { return this._threads.get(handle); } @@ -759,16 +865,25 @@ function convertFromComment(comment: modes.Comment): vscode.Comment { count: reaction.count, hasReacted: reaction.hasReacted }; - }) : undefined + }) : undefined, + mode: comment.mode ? comment.mode : modes.CommentMode.Preview }; } -function convertToModeComment(commentController: ExtHostCommentController, vscodeComment: vscode.Comment, commandsConverter: CommandsConverter): modes.Comment { - const iconPath = vscodeComment.author && vscodeComment.author.iconPath ? vscodeComment.author.iconPath.toString() : - (vscodeComment.userIconPath ? vscodeComment.userIconPath.toString() : vscodeComment.gravatar); +function convertToModeComment2(thread: ExtHostCommentThread, commentController: ExtHostCommentController, vscodeComment: vscode.Comment, commandsConverter: CommandsConverter, commentsMap: Map): modes.Comment { + let commentUniqueId = commentsMap.get(vscodeComment)!; + if (!commentUniqueId) { + commentUniqueId = ++thread.commentHandle; + commentsMap.set(vscodeComment, commentUniqueId); + } + + const iconPath = vscodeComment.author && vscodeComment.author.iconPath ? vscodeComment.author.iconPath.toString() : undefined; return { commentId: vscodeComment.id || vscodeComment.commentId, + mode: vscodeComment.mode, + contextValue: vscodeComment.contextValue, + uniqueIdInThread: commentUniqueId, body: extHostTypeConverter.MarkdownString.from(vscodeComment.body), userName: vscodeComment.author ? vscodeComment.author.name : vscodeComment.userName, userIconPath: iconPath, diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index d2e831b206..1b9d8bab4e 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2297,6 +2297,12 @@ export enum CommentThreadCollapsibleState { */ Expanded = 1 } + +export enum CommentMode { + Editing = 0, + Preview = 1 +} + //#endregion @es5ClassCompat diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index f30104cc48..9e6d686543 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -51,6 +51,10 @@ namespace schema { case 'notebook/toolbar': return MenuId.NotebookToolbar; case 'dataExplorer/context': return MenuId.DataExplorerContext; case 'dataExplorer/action': return MenuId.DataExplorerAction; + case 'comments/commentThread/title': return MenuId.CommentThreadTitle; + case 'comments/commentThread/context': return MenuId.CommentThreadActions; + case 'comments/comment/title': return MenuId.CommentTitle; + case 'comments/comment/context': return MenuId.CommentActions; } return undefined; @@ -196,7 +200,27 @@ namespace schema { description: localize('view.itemContext', "The contributed view item context menu"), type: 'array', items: menuItem - } + }, + 'comments/commentThread/title': { + description: localize('commentThread.title', "The contributed comment thread title menu"), + type: 'array', + items: menuItem + }, + 'comments/commentThread/actions': { + description: localize('commentThread.actions', "The contributed comment thread actions"), + type: 'array', + items: menuItem + }, + 'comments/comment/title': { + description: localize('comment.title', "The contributed comment title menu"), + type: 'array', + items: menuItem + }, + 'comments/comment/actions': { + description: localize('comment.actions', "The contributed comment actions"), + type: 'array', + items: menuItem + }, } }; diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index c802c9d35c..034baa1f27 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -681,6 +681,8 @@ export function createApiFactory( } }; + const comments = comment; + // {{SQL CARBON EDIT}} -- no-op debug extensibility API // namespace: debug const debug: typeof vscode.debug = { @@ -766,6 +768,7 @@ export function createApiFactory( languages, scm, comment, + comments, tasks, window, workspace, @@ -781,6 +784,7 @@ export function createApiFactory( ColorInformation: extHostTypes.ColorInformation, ColorPresentation: extHostTypes.ColorPresentation, CommentThreadCollapsibleState: extHostTypes.CommentThreadCollapsibleState, + CommentMode: extHostTypes.CommentMode, CompletionItem: extHostTypes.CompletionItem, CompletionItemKind: extHostTypes.CompletionItemKind, CompletionList: extHostTypes.CompletionList, diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index fb365946a1..51dbe1ce28 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -302,6 +302,24 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.collapseAll', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: KeyMod.CtrlCmd | KeyCode.LeftArrow, + mac: { + primary: KeyMod.CtrlCmd | KeyCode.LeftArrow, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow] + }, + handler: (accessor) => { + const focusedTree = accessor.get(IListService).lastFocusedList; + + if (focusedTree && !(focusedTree instanceof List || focusedTree instanceof PagedList)) { + focusedTree.collapseAll(); + } + } +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.expand', weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/browser/actions/navigationActions.ts b/src/vs/workbench/browser/actions/navigationActions.ts index 5b08179a85..e9908913d3 100644 --- a/src/vs/workbench/browser/actions/navigationActions.ts +++ b/src/vs/workbench/browser/actions/navigationActions.ts @@ -73,7 +73,7 @@ abstract class BaseNavigationAction extends Action { return this.panelService.openPanel(activePanelId, true)!; } - protected navigateToSidebar(): Promise { + protected async navigateToSidebar(): Promise { if (!this.layoutService.isVisible(Parts.SIDEBAR_PART)) { return Promise.resolve(false); } @@ -84,8 +84,8 @@ abstract class BaseNavigationAction extends Action { } const activeViewletId = activeViewlet.getId(); - return this.viewletService.openViewlet(activeViewletId, true) - .then(value => value === null ? false : value); + const value = await this.viewletService.openViewlet(activeViewletId, true); + return value === null ? false : value; } protected navigateAcrossEditorGroup(direction: GroupDirection): boolean { diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index 99dc626b6c..77b98a15f7 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -160,21 +160,18 @@ export class GlobalRemoveRootFolderAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const state = this.contextService.getWorkbenchState(); // Workspace / Folder if (state === WorkbenchState.WORKSPACE || state === WorkbenchState.FOLDER) { - return this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID).then(folder => { - if (folder) { - return this.workspaceEditingService.removeFolders([folder.uri]).then(() => true); - } - - return true; - }); + const folder = await this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID); + if (folder) { + await this.workspaceEditingService.removeFolders([folder.uri]); + } } - return Promise.resolve(true); + return true; } } @@ -193,20 +190,18 @@ export class SaveWorkspaceAsAction extends Action { super(id, label); } - run(): Promise { - return this.workspaceEditingService.pickNewWorkspacePath().then((configPathUri): Promise | void => { - if (configPathUri) { - switch (this.contextService.getWorkbenchState()) { - case WorkbenchState.EMPTY: - case WorkbenchState.FOLDER: - const folders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri })); - return this.workspaceEditingService.createAndEnterWorkspace(folders, configPathUri); - - case WorkbenchState.WORKSPACE: - return this.workspaceEditingService.saveAndEnterWorkspace(configPathUri); - } + async run(): Promise { + const configPathUri = await this.workspaceEditingService.pickNewWorkspacePath(); + if (configPathUri) { + switch (this.contextService.getWorkbenchState()) { + case WorkbenchState.EMPTY: + case WorkbenchState.FOLDER: + const folders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri })); + return this.workspaceEditingService.createAndEnterWorkspace(folders, configPathUri); + case WorkbenchState.WORKSPACE: + return this.workspaceEditingService.saveAndEnterWorkspace(configPathUri); } - }); + } } } @@ -296,14 +291,13 @@ export class DuplicateWorkspaceInNewWindowAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const folders = this.workspaceContextService.getWorkspace().folders; const remoteAuthority = this.environmentService.configuration.remoteAuthority; - return this.workspacesService.createUntitledWorkspace(folders, remoteAuthority).then(newWorkspace => { - return this.workspaceEditingService.copyWorkspaceSettings(newWorkspace).then(() => { - return this.windowService.openWindow([{ workspaceUri: newWorkspace.configPath }], { forceNewWindow: true }); - }); - }); + const newWorkspace = await this.workspacesService.createUntitledWorkspace(folders, remoteAuthority); + await this.workspaceEditingService.copyWorkspaceSettings(newWorkspace); + + return this.windowService.openWindow([{ workspaceUri: newWorkspace.configPath }], { forceNewWindow: true }); } } diff --git a/src/vs/workbench/browser/actions/workspaceCommands.ts b/src/vs/workbench/browser/actions/workspaceCommands.ts index c06620ae81..9d4ef206f4 100644 --- a/src/vs/workbench/browser/actions/workspaceCommands.ts +++ b/src/vs/workbench/browser/actions/workspaceCommands.ts @@ -54,30 +54,28 @@ CommandsRegistry.registerCommand({ CommandsRegistry.registerCommand({ id: ADD_ROOT_FOLDER_COMMAND_ID, - handler: (accessor) => { + handler: async (accessor) => { const viewletService = accessor.get(IViewletService); const workspaceEditingService = accessor.get(IWorkspaceEditingService); const dialogsService = accessor.get(IFileDialogService); - return dialogsService.showOpenDialog({ + const folders = await dialogsService.showOpenDialog({ openLabel: mnemonicButtonLabel(nls.localize({ key: 'add', comment: ['&& denotes a mnemonic'] }, "&&Add")), title: nls.localize('addFolderToWorkspaceTitle', "Add Folder to Workspace"), canSelectFolders: true, canSelectMany: true, defaultUri: dialogsService.defaultFolderPath() - }).then((folders): Promise | null => { - if (!folders || !folders.length) { - return null; - } - - // Add and show Files Explorer viewlet - return workspaceEditingService.addFolders(folders.map(folder => ({ uri: resources.removeTrailingPathSeparator(folder) }))) - .then(() => viewletService.openViewlet(viewletService.getDefaultViewletId(), true)) - .then(() => undefined); }); + + if (!folders || !folders.length) { + return; + } + + await workspaceEditingService.addFolders(folders.map(folder => ({ uri: resources.removeTrailingPathSeparator(folder) }))); + await viewletService.openViewlet(viewletService.getDefaultViewletId(), true); } }); -CommandsRegistry.registerCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, function (accessor, args?: [IPickOptions, CancellationToken]) { +CommandsRegistry.registerCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, async function (accessor, args?: [IPickOptions, CancellationToken]) { const quickInputService = accessor.get(IQuickInputService); const labelService = accessor.get(ILabelService); const contextService = accessor.get(IWorkspaceContextService); @@ -86,7 +84,7 @@ CommandsRegistry.registerCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, function (acc const folders = contextService.getWorkspace().folders; if (!folders.length) { - return undefined; + return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check } const folderPicks: IQuickPickItem[] = folders.map(folder => { @@ -113,12 +111,11 @@ CommandsRegistry.registerCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, function (acc } const token: CancellationToken = (args ? args[1] : undefined) || CancellationToken.None; + const pick = await quickInputService.pick(folderPicks, options, token); - return quickInputService.pick(folderPicks, options, token).then(pick => { - if (!pick) { - return undefined; - } - + if (pick) { return folders[folderPicks.indexOf(pick)]; - }); + } + + return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check }); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 03abb9d7d8..23ec19a94a 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -170,53 +170,52 @@ export class ResourcesDropHandler { ) { } - handleDrop(event: DragEvent, resolveTargetGroup: () => IEditorGroup | undefined, afterDrop: (targetGroup: IEditorGroup | undefined) => void, targetIndex?: number): void { + async handleDrop(event: DragEvent, resolveTargetGroup: () => IEditorGroup | undefined, afterDrop: (targetGroup: IEditorGroup | undefined) => void, targetIndex?: number): Promise { const untitledOrFileResources = extractResources(event).filter(r => this.fileService.canHandleResource(r.resource) || r.resource.scheme === Schemas.untitled); if (!untitledOrFileResources.length) { return; } // Make the window active to handle the drop properly within - this.windowService.focusWindow().then(() => { + await this.windowService.focusWindow(); - // Check for special things being dropped - return this.doHandleDrop(untitledOrFileResources).then(isWorkspaceOpening => { - if (isWorkspaceOpening) { - return undefined; // return early if the drop operation resulted in this window changing to a workspace - } + // Check for special things being dropped + const isWorkspaceOpening = await this.doHandleDrop(untitledOrFileResources); - // Add external ones to recently open list unless dropped resource is a workspace - const recents: IRecentFile[] = untitledOrFileResources.filter(d => d.isExternal && d.resource.scheme === Schemas.file).map(d => ({ fileUri: d.resource })); - if (recents.length) { - this.windowsService.addRecentlyOpened(recents); - } + if (isWorkspaceOpening) { + return; // return early if the drop operation resulted in this window changing to a workspace + } - const editors: IResourceEditor[] = untitledOrFileResources.map(untitledOrFileResource => ({ - resource: untitledOrFileResource.resource, - options: { - pinned: true, - index: targetIndex, - viewState: (untitledOrFileResource as IDraggedEditor).viewState - } - })); + // Add external ones to recently open list unless dropped resource is a workspace + const recents: IRecentFile[] = untitledOrFileResources.filter(d => d.isExternal && d.resource.scheme === Schemas.file).map(d => ({ fileUri: d.resource })); + if (recents.length) { + this.windowsService.addRecentlyOpened(recents); + } - // Open in Editor - const targetGroup = resolveTargetGroup(); - return this.editorService.openEditors(editors, targetGroup).then(() => { + const editors: IResourceEditor[] = untitledOrFileResources.map(untitledOrFileResource => ({ + resource: untitledOrFileResource.resource, + options: { + pinned: true, + index: targetIndex, + viewState: (untitledOrFileResource as IDraggedEditor).viewState + } + })); - // Finish with provided function - afterDrop(targetGroup); - }); - }); - }); + // Open in Editor + const targetGroup = resolveTargetGroup(); + await this.editorService.openEditors(editors, targetGroup); + + // Finish with provided function + afterDrop(targetGroup); } - private doHandleDrop(untitledOrFileResources: Array): Promise { + private async doHandleDrop(untitledOrFileResources: Array): Promise { // Check for dirty editors being dropped const resourcesWithBackups: IDraggedEditor[] = untitledOrFileResources.filter(resource => !resource.isExternal && !!(resource as IDraggedEditor).backupResource); if (resourcesWithBackups.length > 0) { - return Promise.all(resourcesWithBackups.map(resourceWithBackup => this.handleDirtyEditorDrop(resourceWithBackup))).then(() => false); + await Promise.all(resourcesWithBackups.map(resourceWithBackup => this.handleDirtyEditorDrop(resourceWithBackup))); + return false; } // Check for workspace file being dropped if we are allowed to do so @@ -227,10 +226,10 @@ export class ResourcesDropHandler { } } - return Promise.resolve(false); + return false; } - private handleDirtyEditorDrop(droppedDirtyEditor: IDraggedEditor): Promise { + private async handleDirtyEditorDrop(droppedDirtyEditor: IDraggedEditor): Promise { // Untitled: always ensure that we open a new untitled for each file we drop if (droppedDirtyEditor.resource.scheme === Schemas.untitled) { @@ -239,15 +238,18 @@ export class ResourcesDropHandler { // Return early if the resource is already dirty in target or opened already if (this.textFileService.isDirty(droppedDirtyEditor.resource) || this.editorService.isOpen({ resource: droppedDirtyEditor.resource })) { - return Promise.resolve(false); + return false; } // Resolve the contents of the dropped dirty resource from source - return this.backupFileService.resolveBackupContent(droppedDirtyEditor.backupResource!).then(content => { + try { + const content = await this.backupFileService.resolveBackupContent((droppedDirtyEditor.backupResource!)); + await this.backupFileService.backupResource(droppedDirtyEditor.resource, content.value.create(this.getDefaultEOL()).createSnapshot(true)); + } catch (e) { + // Ignore error + } - // Set the contents of to the resource to the target - return this.backupFileService.backupResource(droppedDirtyEditor.resource, content.value.create(this.getDefaultEOL()).createSnapshot(true)); - }).then(() => false, () => false /* ignore any error */); + return false; } private getDefaultEOL(): DefaultEndOfLine { @@ -259,44 +261,50 @@ export class ResourcesDropHandler { return DefaultEndOfLine.LF; } - private handleWorkspaceFileDrop(fileOnDiskResources: URI[]): Promise { + private async handleWorkspaceFileDrop(fileOnDiskResources: URI[]): Promise { const urisToOpen: IURIToOpen[] = []; const folderURIs: IWorkspaceFolderCreationData[] = []; - return Promise.all(fileOnDiskResources.map(fileOnDiskResource => { + await Promise.all(fileOnDiskResources.map(async fileOnDiskResource => { // Check for Workspace if (hasWorkspaceFileExtension(fileOnDiskResource)) { urisToOpen.push({ workspaceUri: fileOnDiskResource }); - return undefined; + return; } // Check for Folder - return this.fileService.resolve(fileOnDiskResource).then(stat => { + try { + const stat = await this.fileService.resolve(fileOnDiskResource); if (stat.isDirectory) { urisToOpen.push({ folderUri: stat.resource }); folderURIs.push({ uri: stat.resource }); } - }, error => undefined); - })).then(_ => { - - // Return early if no external resource is a folder or workspace - if (urisToOpen.length === 0) { - return false; + } catch (error) { + // Ignore error } + })); - // Pass focus to window - this.windowService.focusWindow(); + // Return early if no external resource is a folder or workspace + if (urisToOpen.length === 0) { + return false; + } - // Open in separate windows if we drop workspaces or just one folder - if (urisToOpen.length > folderURIs.length || folderURIs.length === 1) { - return this.windowService.openWindow(urisToOpen, { forceReuseWindow: true }).then(_ => true); - } + // Pass focus to window + this.windowService.focusWindow(); - // folders.length > 1: Multiple folders: Create new workspace with folders and open - return this.workspaceEditingService.createAndEnterWorkspace(folderURIs).then(_ => true); - }); + // Open in separate windows if we drop workspaces or just one folder + if (urisToOpen.length > folderURIs.length || folderURIs.length === 1) { + await this.windowService.openWindow(urisToOpen, { forceReuseWindow: true }); + } + + // folders.length > 1: Multiple folders: Create new workspace with folders and open + else { + await this.workspaceEditingService.createAndEnterWorkspace(folderURIs); + } + + return true; } } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 1a7709f21d..0da847f77c 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -42,15 +42,15 @@ export class ViewletActivityAction extends ActivityAction { super(activity); } - run(event: any): Promise { + async run(event: any): Promise { if (event instanceof MouseEvent && event.button === 2) { - return Promise.resolve(false); // do not run on right click + return false; // do not run on right click } // prevent accident trigger on a doubleclick (to help nervous people) const now = Date.now(); if (now > this.lastRun /* https://github.com/Microsoft/vscode/issues/25830 */ && now - this.lastRun < ViewletActivityAction.preventDoubleClickDelay) { - return Promise.resolve(true); + return true; } this.lastRun = now; @@ -61,11 +61,12 @@ export class ViewletActivityAction extends ActivityAction { if (sideBarVisible && activeViewlet && activeViewlet.getId() === this.activity.id) { this.logAction('hide'); this.layoutService.setSideBarHidden(true); - return Promise.resolve(); + return true; } this.logAction('show'); - return this.viewletService.openViewlet(this.activity.id, true).then(() => this.activate()); + await this.viewletService.openViewlet(this.activity.id, true); + return this.activate(); } private logAction(action: string) { diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index aab53093e4..7f21c71b8e 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -204,12 +204,13 @@ export class CompositeBar extends Widget implements ICompositeBar { return toDisposable(() => this.model.removeActivity(compositeId, activity)); } - pin(compositeId: string, open?: boolean): void { + async pin(compositeId: string, open?: boolean): Promise { if (this.model.setPinned(compositeId, true)) { this.updateCompositeSwitcher(); if (open) { - this.options.openComposite(compositeId).then(() => this.activateComposite(compositeId)); // Activate after opening + await this.options.openComposite(compositeId); + this.activateComposite(compositeId); // Activate after opening } } } diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index fb7703439e..74b9153fd8 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -74,44 +74,33 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { parent.appendChild(this.scrollbar.getDomNode()); } - setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise { - return super.setInput(input, options, token).then(() => { - return input.resolve().then(model => { + async setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise { + await super.setInput(input, options, token); + const model = await input.resolve(); - // Check for cancellation - if (token.isCancellationRequested) { - return undefined; - } + // Check for cancellation + if (token.isCancellationRequested) { + return; + } - // Assert Model instance - if (!(model instanceof BinaryEditorModel)) { - return Promise.reject(new Error('Unable to open file as binary')); - } + // Assert Model instance + if (!(model instanceof BinaryEditorModel)) { + throw new Error('Unable to open file as binary'); + } - // Render Input - this.resourceViewerContext = ResourceViewer.show( - { name: model.getName(), resource: model.getResource(), size: model.getSize(), etag: model.getETag(), mime: model.getMime() }, - this.textFileService, - this.binaryContainer, - this.scrollbar, - { - openInternalClb: _ => this.handleOpenInternalCallback(input, options), - openExternalClb: this.environmentService.configuration.remoteAuthority ? undefined : resource => this.callbacks.openExternal(resource), - metadataClb: meta => this.handleMetadataChanged(meta) - } - ); - - return undefined; - }); + // Render Input + this.resourceViewerContext = ResourceViewer.show({ name: model.getName(), resource: model.getResource(), size: model.getSize(), etag: model.getETag(), mime: model.getMime() }, this.textFileService, this.binaryContainer, this.scrollbar, { + openInternalClb: () => this.handleOpenInternalCallback(input, options), + openExternalClb: this.environmentService.configuration.remoteAuthority ? undefined : resource => this.callbacks.openExternal(resource), + metadataClb: meta => this.handleMetadataChanged(meta) }); } - private handleOpenInternalCallback(input: EditorInput, options: EditorOptions) { - this.callbacks.openInternal(input, options).then(() => { + private async handleOpenInternalCallback(input: EditorInput, options: EditorOptions): Promise { + await this.callbacks.openInternal(input, options); - // Signal to listeners that the binary editor has been opened in-place - this._onDidOpenInPlace.fire(); - }); + // Signal to listeners that the binary editor has been opened in-place + this._onDidOpenInPlace.fire(); } private handleMetadataChanged(meta: string | undefined): void { diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 8a48eea3e9..e00c60a156 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -539,23 +539,27 @@ export class RevertAndCloseEditorAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const activeControl = this.editorService.activeControl; if (activeControl) { const editor = activeControl.input; const group = activeControl.group; // first try a normal revert where the contents of the editor are restored - return editor.revert().then(() => group.closeEditor(editor), error => { + try { + await editor.revert(); + } catch (error) { // if that fails, since we are about to close the editor, we accept that // the editor cannot be reverted and instead do a soft revert that just // enables us to close the editor. With this, a user can always close a // dirty editor even when reverting fails. - return editor.revert({ soft: true }).then(() => group.closeEditor(editor)); - }); + await editor.revert({ soft: true }); + } + + group.closeEditor(editor); } - return Promise.resolve(false); + return true; } } @@ -618,7 +622,7 @@ export abstract class BaseCloseAllAction extends Action { return groupsToClose; } - run(): Promise { + async run(): Promise { // Just close all if there are no or one dirty editor if (this.textFileService.getDirty().length < 2) { @@ -626,26 +630,23 @@ export abstract class BaseCloseAllAction extends Action { } // Otherwise ask for combined confirmation - return this.textFileService.confirmSave().then(confirm => { - if (confirm === ConfirmResult.CANCEL) { - return undefined; - } + const confirm = await this.textFileService.confirmSave(); + if (confirm === ConfirmResult.CANCEL) { + return; + } - let saveOrRevertPromise: Promise; - if (confirm === ConfirmResult.DONT_SAVE) { - saveOrRevertPromise = this.textFileService.revertAll(undefined, { soft: true }).then(() => true); - } else { - saveOrRevertPromise = this.textFileService.saveAll(true).then(res => res.results.every(r => !!r.success)); - } + let saveOrRevert: boolean; + if (confirm === ConfirmResult.DONT_SAVE) { + await this.textFileService.revertAll(undefined, { soft: true }); + saveOrRevert = true; + } else { + const res = await this.textFileService.saveAll(true); + saveOrRevert = res.results.every(r => !!r.success); + } - return saveOrRevertPromise.then(success => { - if (success) { - return this.doCloseAll(); - } - - return undefined; - }); - }); + if (saveOrRevert) { + return this.doCloseAll(); + } } protected abstract doCloseAll(): Promise; @@ -684,10 +685,10 @@ export class CloseAllEditorGroupsAction extends BaseCloseAllAction { super(id, label, undefined, textFileService, editorGroupService); } - protected doCloseAll(): Promise { - return Promise.all(this.groupsToClose.map(g => g.closeAllEditors())).then(() => { - this.groupsToClose.forEach(group => this.editorGroupService.removeGroup(group)); - }); + protected async doCloseAll(): Promise { + await Promise.all(this.groupsToClose.map(group => group.closeAllEditors())); + + this.groupsToClose.forEach(group => this.editorGroupService.removeGroup(group)); } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 27c1d6044a..87ad67012e 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -670,19 +670,17 @@ function registerCloseEditorCommands() { } }); - CommandsRegistry.registerCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, (accessor: ServicesAccessor, resourceOrContext: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { + CommandsRegistry.registerCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, async (accessor: ServicesAccessor, resourceOrContext: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupsService); const { group } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); if (group) { - return group.closeAllEditors().then(() => { - if (group.count === 0 && editorGroupService.getGroup(group.id) /* could be gone by now */) { - editorGroupService.removeGroup(group); // only remove group if it is now empty - } - }); - } + await group.closeAllEditors(); - return undefined; + if (group.count === 0 && editorGroupService.getGroup(group.id) /* could be gone by now */) { + editorGroupService.removeGroup(group); // only remove group if it is now empty + } + } }); } diff --git a/src/vs/workbench/browser/parts/editor/editorControl.ts b/src/vs/workbench/browser/parts/editor/editorControl.ts index 4cd4d03a7b..2cd9cebdea 100644 --- a/src/vs/workbench/browser/parts/editor/editorControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorControl.ts @@ -58,7 +58,7 @@ export class EditorControl extends Disposable { return this._activeControl as IVisibleEditor | null; } - openEditor(editor: EditorInput, options?: EditorOptions): Promise { + async openEditor(editor: EditorInput, options?: EditorOptions): Promise { // Editor control const descriptor = Registry.as(EditorExtensions.Editors).getEditor(editor); @@ -68,7 +68,8 @@ export class EditorControl extends Disposable { const control = this.doShowEditorControl(descriptor); // Set input - return this.doSetInput(control, editor, withUndefinedAsNull(options)).then((editorChanged => (({ control, editorChanged })))); + const editorChanged = await this.doSetInput(control, editor, withUndefinedAsNull(options)); + return { control, editorChanged }; } private doShowEditorControl(descriptor: IEditorDescriptor): BaseEditor { @@ -150,7 +151,7 @@ export class EditorControl extends Disposable { this._onDidSizeConstraintsChange.fire(undefined); } - private doSetInput(control: BaseEditor, editor: EditorInput, options: EditorOptions | null): Promise { + private async doSetInput(control: BaseEditor, editor: EditorInput, options: EditorOptions | null): Promise { // If the input did not change, return early and only apply the options // unless the options instruct us to force open it even if it is the same @@ -167,7 +168,7 @@ export class EditorControl extends Disposable { control.focus(); } - return Promise.resolve(false); + return false; } // Show progress while setting input after a certain timeout. If the workbench is opening @@ -176,7 +177,8 @@ export class EditorControl extends Disposable { // Call into editor control const editorWillChange = !inputMatches; - return control.setInput(editor, options, operation.token).then(() => { + try { + await control.setInput(editor, options, operation.token); // Focus (unless prevented or another operation is running) if (operation.isCurrent()) { @@ -186,17 +188,10 @@ export class EditorControl extends Disposable { } } - // Operation done - operation.stop(); - return editorWillChange; - }, e => { - - // Operation done + } finally { operation.stop(); - - return Promise.reject(e); - }); + } } private doHideActiveEditorControl(): void { diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 81b34791d1..05277917f6 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -406,7 +406,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } } - private restoreEditors(from: IEditorGroupView | ISerializedEditorGroup): Promise { + private async restoreEditors(from: IEditorGroupView | ISerializedEditorGroup): Promise { if (this._group.count === 0) { return Promise.resolve(); // nothing to show } @@ -430,16 +430,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const activeElement = document.activeElement; // Show active editor - return this.doShowEditor(activeEditor, true, options).then(() => { + await this.doShowEditor(activeEditor, true, options); - // Set focused now if this is the active group and focus has - // not changed meanwhile. This prevents focus from being - // stolen accidentally on startup when the user already - // clicked somewhere. - if (this.accessor.activeGroup === this && activeElement === document.activeElement) { - this.focus(); - } - }); + // Set focused now if this is the active group and focus has + // not changed meanwhile. This prevents focus from being + // stolen accidentally on startup when the user already + // clicked somewhere. + if (this.accessor.activeGroup === this && activeElement === document.activeElement) { + this.focus(); + } } //#region event handling @@ -821,34 +820,33 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.doShowEditor(editor, !!openEditorOptions.active, options); } - private doShowEditor(editor: EditorInput, active: boolean, options?: EditorOptions): Promise { + private async doShowEditor(editor: EditorInput, active: boolean, options?: EditorOptions): Promise { // Show in editor control if the active editor changed - let openEditorPromise: Promise; + let openEditor: IEditor | null = null; if (active) { - openEditorPromise = this.editorControl.openEditor(editor, options).then(result => { + try { + const result = await this.editorControl.openEditor(editor, options); // Editor change event if (result.editorChanged) { this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_ACTIVE, editor }); } - return result.control; - }, error => { + openEditor = result.control; + } catch (error) { // Handle errors but do not bubble them up this.doHandleOpenEditorError(error, editor, options); - - return null; // error: return NULL as result to signal this - }); + } } else { - openEditorPromise = Promise.resolve(null); // inactive: return NULL as result to signal this + openEditor = null; // inactive: return NULL as result to signal this } // Show in title control after editor control because some actions depend on it this.titleAreaControl.openEditor(editor); - return openEditorPromise; + return openEditor; } private doHandleOpenEditorError(error: Error, editor: EditorInput, options?: EditorOptions): void { @@ -884,37 +882,33 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region openEditors() - openEditors(editors: { editor: EditorInput, options?: EditorOptions }[]): Promise { + async openEditors(editors: { editor: EditorInput, options?: EditorOptions }[]): Promise { if (!editors.length) { - return Promise.resolve(null); + return null; } // Do not modify original array editors = editors.slice(0); - let result: IEditor | null; - // Use the first editor as active editor const { editor, options } = editors.shift()!; - return this.openEditor(editor, options).then(activeEditor => { - result = activeEditor; // this can be NULL if the opening failed + let firstEditor = await this.openEditor(editor, options); - const startingIndex = this.getIndexOfEditor(editor) + 1; + // Open the other ones inactive + const startingIndex = this.getIndexOfEditor(editor) + 1; + await Promise.all(editors.map(async ({ editor, options }, index) => { + const adjustedEditorOptions = options || new EditorOptions(); + adjustedEditorOptions.inactive = true; + adjustedEditorOptions.pinned = true; + adjustedEditorOptions.index = startingIndex + index; - // Open the other ones inactive - return Promise.all(editors.map(({ editor, options }, index) => { - const adjustedEditorOptions = options || new EditorOptions(); - adjustedEditorOptions.inactive = true; - adjustedEditorOptions.pinned = true; - adjustedEditorOptions.index = startingIndex + index; + const openedEditor = await this.openEditor(editor, adjustedEditorOptions); + if (!firstEditor) { + firstEditor = openedEditor; // only take if the first editor opening failed + } + })); - return this.openEditor(editor, adjustedEditorOptions).then(activeEditor => { - if (!result) { - result = activeEditor; // only take if the first editor opening failed - } - }); - })).then(() => result); - }); + return firstEditor; } //#endregion @@ -995,20 +989,19 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region closeEditor() - closeEditor(editor: EditorInput | undefined = this.activeEditor || undefined, options?: ICloseEditorOptions): Promise { + async closeEditor(editor: EditorInput | undefined = this.activeEditor || undefined, options?: ICloseEditorOptions): Promise { if (!editor) { - return Promise.resolve(); + return; } // Check for dirty and veto - return this.handleDirty([editor]).then(veto => { - if (veto) { - return; - } + const veto = await this.handleDirty([editor]); + if (veto) { + return; + } - // Do close - this.doCloseEditor(editor, options && options.preserveFocus ? false : undefined); - }); + // Do close + this.doCloseEditor(editor, options && options.preserveFocus ? false : undefined); } private doCloseEditor(editor: EditorInput, focusNext = (this.accessor.activeGroup === this), fromError?: boolean): void { @@ -1113,7 +1106,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._group.closeEditor(editor); } - private handleDirty(editors: EditorInput[]): Promise { + private async handleDirty(editors: EditorInput[]): Promise { if (!editors.length) { return Promise.resolve(false); // no veto } @@ -1128,22 +1121,21 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.mapEditorToPendingConfirmation.set(editor, handleDirtyPromise); } - return handleDirtyPromise.then(veto => { + const veto = await handleDirtyPromise; - // Make sure to remove from our map of cached pending confirmations - this.mapEditorToPendingConfirmation.delete(editor); + // Make sure to remove from our map of cached pending confirmations + this.mapEditorToPendingConfirmation.delete(editor); - // Return for the first veto we got - if (veto) { - return veto; - } + // Return for the first veto we got + if (veto) { + return veto; + } - // Otherwise continue with the remainders - return this.handleDirty(editors); - }); + // Otherwise continue with the remainders + return this.handleDirty(editors); } - private doHandleDirty(editor: EditorInput): Promise { + private async doHandleDirty(editor: EditorInput): Promise { if ( !editor.isDirty() || // editor must be dirty this.accessor.groups.some(groupView => groupView !== this && groupView.group.contains(editor, true /* support side by side */)) || // editor is opened in other group @@ -1153,59 +1145,65 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Switch to editor that we want to handle and confirm to save/revert - return this.openEditor(editor).then(() => editor.confirmSave().then(res => { + await this.openEditor(editor); - // It could be that the editor saved meanwhile, so we check again - // to see if anything needs to happen before closing for good. - // This can happen for example if autoSave: onFocusChange is configured - // so that the save happens when the dialog opens. - if (!editor.isDirty()) { - return res === ConfirmResult.CANCEL ? true : false; - } + const res = await editor.confirmSave(); - // Otherwise, handle accordingly - switch (res) { - case ConfirmResult.SAVE: - return editor.save().then(ok => !ok); + // It could be that the editor saved meanwhile, so we check again + // to see if anything needs to happen before closing for good. + // This can happen for example if autoSave: onFocusChange is configured + // so that the save happens when the dialog opens. + if (!editor.isDirty()) { + return res === ConfirmResult.CANCEL ? true : false; + } - case ConfirmResult.DONT_SAVE: + // Otherwise, handle accordingly + switch (res) { + case ConfirmResult.SAVE: + const result = await editor.save(); + + return !result; + case ConfirmResult.DONT_SAVE: + + try { // first try a normal revert where the contents of the editor are restored - return editor.revert().then(ok => !ok, error => { + const result = await editor.revert(); - // if that fails, since we are about to close the editor, we accept that - // the editor cannot be reverted and instead do a soft revert that just - // enables us to close the editor. With this, a user can always close a - // dirty editor even when reverting fails. - return editor.revert({ soft: true }).then(ok => !ok); - }); + return !result; + } catch (error) { + // if that fails, since we are about to close the editor, we accept that + // the editor cannot be reverted and instead do a soft revert that just + // enables us to close the editor. With this, a user can always close a + // dirty editor even when reverting fails. + const result = await editor.revert({ soft: true }); - case ConfirmResult.CANCEL: - return true; // veto - } - })); + return !result; + } + case ConfirmResult.CANCEL: + return true; // veto + } } //#endregion //#region closeEditors() - closeEditors(args: EditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise { + async closeEditors(args: EditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise { if (this.isEmpty()) { - return Promise.resolve(); + return; } const editors = this.getEditorsToClose(args); // Check for dirty and veto - return this.handleDirty(editors.slice(0)).then(veto => { - if (veto) { - return; - } + const veto = await this.handleDirty(editors.slice(0)); + if (veto) { + return; + } - // Do close - this.doCloseEditors(editors, options); - }); + // Do close + this.doCloseEditors(editors, options); } private getEditorsToClose(editors: EditorInput[] | ICloseEditorsFilter): EditorInput[] { @@ -1263,7 +1261,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region closeAllEditors() - closeAllEditors(): Promise { + async closeAllEditors(): Promise { if (this.isEmpty()) { // If the group is empty and the request is to close all editors, we still close @@ -1273,19 +1271,18 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.accessor.removeGroup(this); } - return Promise.resolve(); + return; } // Check for dirty and veto const editors = this._group.getEditors(true); - return this.handleDirty(editors.slice(0)).then(veto => { - if (veto) { - return; - } + const veto = await this.handleDirty(editors.slice(0)); + if (veto) { + return; + } - // Do close - this.doCloseAllEditors(); - }); + // Do close + this.doCloseAllEditors(); } private doCloseAllEditors(): void { @@ -1308,7 +1305,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region replaceEditors() - replaceEditors(editors: EditorReplacement[]): Promise { + async replaceEditors(editors: EditorReplacement[]): Promise { // Extract active vs. inactive replacements let activeReplacement: EditorReplacement | undefined; @@ -1366,10 +1363,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.titleAreaControl.closeEditor(activeReplacement.editor); } - return openEditorResult.then(() => undefined); + await openEditorResult; } - - return Promise.resolve(); } //#endregion diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 39dbaacb3b..32f4cced01 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -858,8 +858,8 @@ export class ShowLanguageExtensionsAction extends Action { this.enabled = galleryService.isEnabled(); } - run(): Promise { - return this.commandService.executeCommand('workbench.extensions.action.showExtensionsForLanguage', this.fileExtension).then(() => undefined); + async run(): Promise { + await this.commandService.executeCommand('workbench.extensions.action.showExtensionsForLanguage', this.fileExtension); } } @@ -883,7 +883,7 @@ export class ChangeModeAction extends Action { super(actionId, actionLabel); } - run(): Promise { + async run(): Promise { const activeTextEditorWidget = getCodeEditor(this.editorService.activeTextEditorWidget); if (!activeTextEditorWidget) { return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); @@ -1038,31 +1038,30 @@ export class ChangeModeAction extends Action { }; }); - setTimeout(() => { - this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || base) }).then(language => { - if (language) { - const fileAssociationsConfig = this.configurationService.inspect<{}>(FILES_ASSOCIATIONS_CONFIG); + setTimeout(async () => { + const language = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || base) }); + if (language) { + const fileAssociationsConfig = this.configurationService.inspect<{}>(FILES_ASSOCIATIONS_CONFIG); - let associationKey: string; - if (extension && base[0] !== '.') { - associationKey = `*${extension}`; // only use "*.ext" if the file path is in the form of . - } else { - associationKey = base; // otherwise use the basename (e.g. .gitignore, Dockerfile) - } - - // If the association is already being made in the workspace, make sure to target workspace settings - let target = ConfigurationTarget.USER; - if (fileAssociationsConfig.workspace && !!fileAssociationsConfig.workspace[associationKey]) { - target = ConfigurationTarget.WORKSPACE; - } - - // Make sure to write into the value of the target and not the merged value from USER and WORKSPACE config - const currentAssociations = deepClone((target === ConfigurationTarget.WORKSPACE) ? fileAssociationsConfig.workspace : fileAssociationsConfig.user) || Object.create(null); - currentAssociations[associationKey] = language.id; - - this.configurationService.updateValue(FILES_ASSOCIATIONS_CONFIG, currentAssociations, target); + let associationKey: string; + if (extension && base[0] !== '.') { + associationKey = `*${extension}`; // only use "*.ext" if the file path is in the form of . + } else { + associationKey = base; // otherwise use the basename (e.g. .gitignore, Dockerfile) } - }); + + // If the association is already being made in the workspace, make sure to target workspace settings + let target = ConfigurationTarget.USER; + if (fileAssociationsConfig.workspace && !!fileAssociationsConfig.workspace[associationKey]) { + target = ConfigurationTarget.WORKSPACE; + } + + // Make sure to write into the value of the target and not the merged value from USER and WORKSPACE config + const currentAssociations = deepClone((target === ConfigurationTarget.WORKSPACE) ? fileAssociationsConfig.workspace : fileAssociationsConfig.user) || Object.create(null); + currentAssociations[associationKey] = language.id; + + this.configurationService.updateValue(FILES_ASSOCIATIONS_CONFIG, currentAssociations, target); + } }, 50 /* quick open is sensitive to being opened so soon after another */); } } @@ -1085,7 +1084,7 @@ class ChangeIndentationAction extends Action { super(actionId, actionLabel); } - run(): Promise { + async run(): Promise { const activeTextEditorWidget = getCodeEditor(this.editorService.activeTextEditorWidget); if (!activeTextEditorWidget) { return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); @@ -1117,7 +1116,8 @@ class ChangeIndentationAction extends Action { picks.splice(3, 0, { type: 'separator', label: nls.localize('indentConvert', "convert file") }); picks.unshift({ type: 'separator', label: nls.localize('indentView', "change view") }); - return this.quickInputService.pick(picks, { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }).then(action => action && action.run()); + const action = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }); + return action && action.run(); } } @@ -1135,7 +1135,7 @@ export class ChangeEOLAction extends Action { super(actionId, actionLabel); } - run(): Promise { + async run(): Promise { const activeTextEditorWidget = getCodeEditor(this.editorService.activeTextEditorWidget); if (!activeTextEditorWidget) { return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); @@ -1145,7 +1145,7 @@ export class ChangeEOLAction extends Action { return this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]); } - const textModel = activeTextEditorWidget.getModel(); + let textModel = activeTextEditorWidget.getModel(); const EOLOptions: IChangeEOLEntry[] = [ { label: nlsEOLLF, eol: EndOfLineSequence.LF }, @@ -1154,15 +1154,14 @@ export class ChangeEOLAction extends Action { const selectedIndex = (textModel && textModel.getEOL() === '\n') ? 0 : 1; - return this.quickInputService.pick(EOLOptions, { placeHolder: nls.localize('pickEndOfLine', "Select End of Line Sequence"), activeItem: EOLOptions[selectedIndex] }).then(eol => { - if (eol) { - const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorWidget); - if (activeCodeEditor && activeCodeEditor.hasModel() && isWritableCodeEditor(activeCodeEditor)) { - const textModel = activeCodeEditor.getModel(); - textModel.pushEOL(eol.eol); - } + const eol = await this.quickInputService.pick(EOLOptions, { placeHolder: nls.localize('pickEndOfLine', "Select End of Line Sequence"), activeItem: EOLOptions[selectedIndex] }); + if (eol) { + const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorWidget); + if (activeCodeEditor && activeCodeEditor.hasModel() && isWritableCodeEditor(activeCodeEditor)) { + textModel = activeCodeEditor.getModel(); + textModel.pushEOL(eol.eol); } - }); + } } } diff --git a/src/vs/workbench/browser/parts/editor/resourceViewer.ts b/src/vs/workbench/browser/parts/editor/resourceViewer.ts index 31b92e92f0..ec9bb061ff 100644 --- a/src/vs/workbench/browser/parts/editor/resourceViewer.ts +++ b/src/vs/workbench/browser/parts/editor/resourceViewer.ts @@ -567,16 +567,15 @@ class InlineImageView { return context; } - private static imageSrc(descriptor: IResourceDescriptor, textFileService: ITextFileService): Promise { + private static async imageSrc(descriptor: IResourceDescriptor, textFileService: ITextFileService): Promise { if (descriptor.resource.scheme === Schemas.data) { return Promise.resolve(descriptor.resource.toString(true /* skip encoding */)); } - return textFileService.read(descriptor.resource, { encoding: 'base64' }).then(data => { - const mime = getMime(descriptor); + const data = await textFileService.read(descriptor.resource, { encoding: 'base64' }); + const mime = getMime(descriptor); - return `data:${mime};base64,${data.value}`; - }); + return `data:${mime};base64,${data.value}`; } } diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index 8c1006415d..f4411f624c 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -93,10 +93,11 @@ export class SideBySideEditor extends BaseEditor { this.updateStyles(); } - setInput(newInput: EditorInput, options: EditorOptions, token: CancellationToken): Promise { + async setInput(newInput: EditorInput, options: EditorOptions, token: CancellationToken): Promise { const oldInput = this.input as SideBySideEditorInput; - return super.setInput(newInput, options, token) - .then(() => this.updateInput(oldInput, newInput as SideBySideEditorInput, options, token)); + await super.setInput(newInput, options, token); + + return this.updateInput(oldInput, (newInput as SideBySideEditorInput), options, token); } setOptions(options: EditorOptions): void { @@ -158,7 +159,7 @@ export class SideBySideEditor extends BaseEditor { return this.detailsEditor; } - private updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options: EditorOptions, token: CancellationToken): Promise { + private async updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options: EditorOptions, token: CancellationToken): Promise { if (!newInput.matches(oldInput)) { if (oldInput) { this.disposeEditors(); @@ -166,14 +167,15 @@ export class SideBySideEditor extends BaseEditor { return this.setNewInput(newInput, options, token); } + if (!this.detailsEditor || !this.masterEditor) { return Promise.resolve(); } - return Promise.all([ + await Promise.all([ this.detailsEditor.setInput(newInput.details, null, token), - this.masterEditor.setInput(newInput.master, options, token)] - ).then(() => undefined); + this.masterEditor.setInput(newInput.master, options, token) + ]); } private setNewInput(newInput: SideBySideEditorInput, options: EditorOptions, token: CancellationToken): Promise { @@ -196,7 +198,7 @@ export class SideBySideEditor extends BaseEditor { return editor; } - private onEditorsCreated(details: BaseEditor, master: BaseEditor, detailsInput: EditorInput, masterInput: EditorInput, options: EditorOptions, token: CancellationToken): Promise { + private async onEditorsCreated(details: BaseEditor, master: BaseEditor, detailsInput: EditorInput, masterInput: EditorInput, options: EditorOptions, token: CancellationToken): Promise { this.detailsEditor = details; this.masterEditor = master; @@ -207,7 +209,12 @@ export class SideBySideEditor extends BaseEditor { this.onDidCreateEditors.fire(undefined); - return Promise.all([this.detailsEditor.setInput(detailsInput, null, token), this.masterEditor.setInput(masterInput, options, token)]).then(() => this.focus()); + await Promise.all([ + this.detailsEditor.setInput(detailsInput, null, token), + this.masterEditor.setInput(masterInput, options, token)] + ); + + return this.focus(); } updateStyles(): void { diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 3c977432ac..79cbb7b43a 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -84,7 +84,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration); } - setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise { // Dispose previous diff navigator this.diffNavigatorDisposables = dispose(this.diffNavigatorDisposables); @@ -93,56 +93,55 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { this.saveTextDiffEditorViewState(this.input); // Set input and resolve - return super.setInput(input, options, token).then(() => { - return input.resolve().then(resolvedModel => { + await super.setInput(input, options, token); - // Check for cancellation - if (token.isCancellationRequested) { - return undefined; - } + try { + const resolvedModel = await input.resolve(); - // Assert Model Instance - if (!(resolvedModel instanceof TextDiffEditorModel) && this.openAsBinary(input, options)) { - return undefined; - } - - // Set Editor Model - const diffEditor = this.getControl(); - const resolvedDiffEditorModel = resolvedModel; - diffEditor.setModel(resolvedDiffEditorModel.textDiffEditorModel); - - // Apply Options from TextOptions - let optionsGotApplied = false; - if (options && types.isFunction((options).apply)) { - optionsGotApplied = (options).apply(diffEditor, ScrollType.Immediate); - } - - // Otherwise restore View State - let hasPreviousViewState = false; - if (!optionsGotApplied) { - hasPreviousViewState = this.restoreTextDiffEditorViewState(input); - } - - // Diff navigator - this.diffNavigator = new DiffNavigator(diffEditor, { - alwaysRevealFirst: !optionsGotApplied && !hasPreviousViewState // only reveal first change if we had no options or viewstate - }); - this.diffNavigatorDisposables.push(this.diffNavigator); - - // Readonly flag - diffEditor.updateOptions({ readOnly: resolvedDiffEditorModel.isReadonly() }); + // Check for cancellation + if (token.isCancellationRequested) { return undefined; - }, error => { + } - // In case we tried to open a file and the response indicates that this is not a text file, fallback to binary diff. - if (this.isFileBinaryError(error) && this.openAsBinary(input, options)) { - return null; - } + // Assert Model Instance + if (!(resolvedModel instanceof TextDiffEditorModel) && this.openAsBinary(input, options)) { + return undefined; + } - // Otherwise make sure the error bubbles up - return Promise.reject(error); + // Set Editor Model + const diffEditor = this.getControl(); + const resolvedDiffEditorModel = resolvedModel; + diffEditor.setModel(resolvedDiffEditorModel.textDiffEditorModel); + + // Apply Options from TextOptions + let optionsGotApplied = false; + if (options && types.isFunction((options).apply)) { + optionsGotApplied = (options).apply(diffEditor, ScrollType.Immediate); + } + + // Otherwise restore View State + let hasPreviousViewState = false; + if (!optionsGotApplied) { + hasPreviousViewState = this.restoreTextDiffEditorViewState(input); + } + + // Diff navigator + this.diffNavigator = new DiffNavigator(diffEditor, { + alwaysRevealFirst: !optionsGotApplied && !hasPreviousViewState // only reveal first change if we had no options or viewstate }); - }); + this.diffNavigatorDisposables.push(this.diffNavigator); + + // Readonly flag + diffEditor.updateOptions({ readOnly: resolvedDiffEditorModel.isReadonly() }); + } catch (error) { + + // In case we tried to open a file and the response indicates that this is not a text file, fallback to binary diff. + if (this.isFileBinaryError(error) && this.openAsBinary(input, options)) { + return; + } + + throw error; + } } setOptions(options: EditorOptions): void { diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index 20ccc92db4..3fe55be0bb 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -191,14 +191,13 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { return this.instantiationService.createInstance(CodeEditorWidget, parent, configuration, {}); } - setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise { - return super.setInput(input, options, token).then(() => { + async setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise { + await super.setInput(input, options, token); - // Update editor options after having set the input. We do this because there can be - // editor input specific options (e.g. an ARIA label depending on the input showing) - this.updateEditorConfiguration(); - this._editorContainer.setAttribute('aria-label', this.computeAriaLabel()); - }); + // Update editor options after having set the input. We do this because there can be + // editor input specific options (e.g. an ARIA label depending on the input showing) + this.updateEditorConfiguration(); + this._editorContainer.setAttribute('aria-label', this.computeAriaLabel()); } protected setEditorVisible(visible: boolean, group: IEditorGroup): void { diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 1b79a8eef0..155fa3c38e 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { TextEditorOptions, EditorModel, EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { TextEditorOptions, EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; @@ -54,45 +54,41 @@ export class AbstractTextResourceEditor extends BaseTextEditor { return nls.localize('textEditor', "Text Editor"); } - setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise { // Remember view settings if input changes this.saveTextResourceEditorViewState(this.input); // Set input and resolve - return super.setInput(input, options, token).then(() => { - return input.resolve().then((resolvedModel: EditorModel) => { + await super.setInput(input, options, token); + const resolvedModel = await input.resolve(); - // Check for cancellation - if (token.isCancellationRequested) { - return undefined; - } + // Check for cancellation + if (token.isCancellationRequested) { + return undefined; + } - // Assert Model instance - if (!(resolvedModel instanceof BaseTextEditorModel)) { - return Promise.reject(new Error('Unable to open file as text')); - } + // Assert Model instance + if (!(resolvedModel instanceof BaseTextEditorModel)) { + return Promise.reject(new Error('Unable to open file as text')); + } - // Set Editor Model - const textEditor = this.getControl(); - const textEditorModel = resolvedModel.textEditorModel; - textEditor.setModel(textEditorModel); + // Set Editor Model + const textEditor = this.getControl(); + const textEditorModel = resolvedModel.textEditorModel; + textEditor.setModel(textEditorModel); - // Apply Options from TextOptions - let optionsGotApplied = false; - const textOptions = options; - if (textOptions && types.isFunction(textOptions.apply)) { - optionsGotApplied = textOptions.apply(textEditor, ScrollType.Immediate); - } + // Apply Options from TextOptions + let optionsGotApplied = false; + const textOptions = options; + if (textOptions && types.isFunction(textOptions.apply)) { + optionsGotApplied = textOptions.apply(textEditor, ScrollType.Immediate); + } - // Otherwise restore View State - if (!optionsGotApplied) { - this.restoreTextResourceEditorViewState(input); - } - - return undefined; - }); - }); + // Otherwise restore View State + if (!optionsGotApplied) { + this.restoreTextResourceEditorViewState(input); + } } private restoreTextResourceEditorViewState(input: EditorInput) { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts index 67302b12b5..ae4a58bdb2 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts @@ -160,7 +160,7 @@ export class NotificationActionRunner extends ActionRunner { super(); } - protected runAction(action: IAction, context: INotificationViewItem): Promise { + protected async runAction(action: IAction, context: INotificationViewItem): Promise { /* __GDPR__ "workbenchActionExecuted" : { @@ -171,8 +171,10 @@ export class NotificationActionRunner extends ActionRunner { this.telemetryService.publicLog('workbenchActionExecuted', { id: action.id, from: 'message' }); // Run and make sure to notify on any error again - super.runAction(action, context).then(undefined, error => this.notificationService.error(error)); - - return Promise.resolve(); + try { + await super.runAction(action, context); + } catch (error) { + this.notificationService.error(error); + } } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 41745f6c05..a7b1f2b0cb 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -93,18 +93,17 @@ export class NotificationsToasts extends Themable { }); } - private onCanShowNotifications(): Promise { + private async onCanShowNotifications(): Promise { // Wait for the running phase to ensure we can draw notifications properly - return this.lifecycleService.when(LifecyclePhase.Ready).then(() => { + await this.lifecycleService.when(LifecyclePhase.Ready); - // Push notificiations out until either workbench is restored - // or some time has ellapsed to reduce pressure on the startup - return Promise.race([ - this.lifecycleService.when(LifecyclePhase.Restored), - timeout(2000) - ]); - }); + // Push notificiations out until either workbench is restored + // or some time has ellapsed to reduce pressure on the startup + return Promise.race([ + this.lifecycleService.when(LifecyclePhase.Restored), + timeout(2000) + ]); } private onDidNotificationChange(e: INotificationChangeEvent): void { diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts index a026ac0775..07fdb8ef9e 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts @@ -259,8 +259,7 @@ export class QuickOpenController extends Component implements IQuickOpenService // Pass to handlers for (let prefix in this.mapResolvedHandlersToPrefix) { - const promise = this.mapResolvedHandlersToPrefix[prefix]; - promise.then(handler => { + this.mapResolvedHandlersToPrefix[prefix].then(handler => { this.handlerOnOpenCalled[prefix] = false; handler.onClose(reason === HideReason.CANCELED); // Don't check if onOpen was called to preserve old behaviour for now @@ -429,7 +428,7 @@ export class QuickOpenController extends Component implements IQuickOpenService }); } - private handleDefaultHandler(handler: QuickOpenHandlerDescriptor, value: string, token: CancellationToken): Promise { + private async handleDefaultHandler(handler: QuickOpenHandlerDescriptor, value: string, token: CancellationToken): Promise { // Fill in history results if matching and we are configured to search in history let matchingHistoryEntries: QuickOpenEntry[]; @@ -444,47 +443,41 @@ export class QuickOpenController extends Component implements IQuickOpenService } // Resolve - return this.resolveHandler(handler).then(resolvedHandler => { - const quickOpenModel = new QuickOpenModel(matchingHistoryEntries, this.actionProvider); + const resolvedHandler = await this.resolveHandler(handler); - let inputSet = false; + const quickOpenModel = new QuickOpenModel(matchingHistoryEntries, this.actionProvider); - // If we have matching entries from history we want to show them directly and not wait for the other results to come in - // This also applies when we used to have entries from a previous run and now there are no more history results matching - const previousInput = this.quickOpenWidget.getInput(); - const wasShowingHistory = previousInput && previousInput.entries && previousInput.entries.some(e => e instanceof EditorHistoryEntry || e instanceof EditorHistoryEntryGroup); - if (wasShowingHistory || matchingHistoryEntries.length > 0) { - let responseDelay: Promise; - if (resolvedHandler.hasShortResponseTime()) { - responseDelay = timeout(QuickOpenController.MAX_SHORT_RESPONSE_TIME); - } else { - responseDelay = Promise.resolve(); - } + let inputSet = false; - responseDelay.then(() => { - if (!token.isCancellationRequested && !inputSet) { - this.quickOpenWidget.setInput(quickOpenModel, { autoFocusFirstEntry: true }); - inputSet = true; - } - }); + // If we have matching entries from history we want to show them directly and not wait for the other results to come in + // This also applies when we used to have entries from a previous run and now there are no more history results matching + const previousInput = this.quickOpenWidget.getInput(); + const wasShowingHistory = previousInput && previousInput.entries && previousInput.entries.some(e => e instanceof EditorHistoryEntry || e instanceof EditorHistoryEntryGroup); + if (wasShowingHistory || matchingHistoryEntries.length > 0) { + if (resolvedHandler.hasShortResponseTime()) { + await timeout(QuickOpenController.MAX_SHORT_RESPONSE_TIME); } - // Get results - return resolvedHandler.getResults(value, token).then(result => { - if (!token.isCancellationRequested) { + if (!token.isCancellationRequested && !inputSet) { + this.quickOpenWidget.setInput(quickOpenModel, { autoFocusFirstEntry: true }); + inputSet = true; + } + } - // now is the time to show the input if we did not have set it before - if (!inputSet) { - this.quickOpenWidget.setInput(quickOpenModel, { autoFocusFirstEntry: true }); - inputSet = true; - } + // Get results + const result = await resolvedHandler.getResults(value, token); + if (!token.isCancellationRequested) { - // merge history and default handler results - const handlerResults = (result && result.entries) || []; - this.mergeResults(quickOpenModel, handlerResults, types.withNullAsUndefined(resolvedHandler.getGroupLabel())); - } - }); - }); + // now is the time to show the input if we did not have set it before + if (!inputSet) { + this.quickOpenWidget.setInput(quickOpenModel, { autoFocusFirstEntry: true }); + inputSet = true; + } + + // merge history and default handler results + const handlerResults = (result && result.entries) || []; + this.mergeResults(quickOpenModel, handlerResults, types.withNullAsUndefined(resolvedHandler.getGroupLabel())); + } } private mergeResults(quickOpenModel: QuickOpenModel, handlerResults: QuickOpenEntry[], groupLabel: string | undefined): void { @@ -516,46 +509,44 @@ export class QuickOpenController extends Component implements IQuickOpenService } } - private handleSpecificHandler(handlerDescriptor: QuickOpenHandlerDescriptor, value: string, token: CancellationToken): Promise { - return this.resolveHandler(handlerDescriptor).then((resolvedHandler: QuickOpenHandler) => { + private async handleSpecificHandler(handlerDescriptor: QuickOpenHandlerDescriptor, value: string, token: CancellationToken): Promise { + const resolvedHandler = await this.resolveHandler(handlerDescriptor); - // Remove handler prefix from search value - value = value.substr(handlerDescriptor.prefix.length); + // Remove handler prefix from search value + value = value.substr(handlerDescriptor.prefix.length); - // Return early if the handler can not run in the current environment and inform the user - const canRun = resolvedHandler.canRun(); - if (types.isUndefinedOrNull(canRun) || (typeof canRun === 'boolean' && !canRun) || typeof canRun === 'string') { - const placeHolderLabel = (typeof canRun === 'string') ? canRun : nls.localize('canNotRunPlaceholder', "This quick open handler can not be used in the current context"); + // Return early if the handler can not run in the current environment and inform the user + const canRun = resolvedHandler.canRun(); + if (types.isUndefinedOrNull(canRun) || (typeof canRun === 'boolean' && !canRun) || typeof canRun === 'string') { + const placeHolderLabel = (typeof canRun === 'string') ? canRun : nls.localize('canNotRunPlaceholder', "This quick open handler can not be used in the current context"); - const model = new QuickOpenModel([new PlaceholderQuickOpenEntry(placeHolderLabel)], this.actionProvider); + const model = new QuickOpenModel([new PlaceholderQuickOpenEntry(placeHolderLabel)], this.actionProvider); + this.showModel(model, resolvedHandler.getAutoFocus(value, { model, quickNavigateConfiguration: this.quickOpenWidget.getQuickNavigateConfiguration() }), types.withNullAsUndefined(resolvedHandler.getAriaLabel())); + + return Promise.resolve(undefined); + } + + // Support extra class from handler + const extraClass = resolvedHandler.getClass(); + if (extraClass) { + this.quickOpenWidget.setExtraClass(extraClass); + } + + // When handlers change, clear the result list first before loading the new results + if (this.previousActiveHandlerDescriptor !== handlerDescriptor) { + this.clearModel(); + } + + // Receive Results from Handler and apply + const result = await resolvedHandler.getResults(value, token); + if (!token.isCancellationRequested) { + if (!result || !result.entries.length) { + const model = new QuickOpenModel([new PlaceholderQuickOpenEntry(resolvedHandler.getEmptyLabel(value))]); this.showModel(model, resolvedHandler.getAutoFocus(value, { model, quickNavigateConfiguration: this.quickOpenWidget.getQuickNavigateConfiguration() }), types.withNullAsUndefined(resolvedHandler.getAriaLabel())); - - return Promise.resolve(undefined); + } else { + this.showModel(result, resolvedHandler.getAutoFocus(value, { model: result, quickNavigateConfiguration: this.quickOpenWidget.getQuickNavigateConfiguration() }), types.withNullAsUndefined(resolvedHandler.getAriaLabel())); } - - // Support extra class from handler - const extraClass = resolvedHandler.getClass(); - if (extraClass) { - this.quickOpenWidget.setExtraClass(extraClass); - } - - // When handlers change, clear the result list first before loading the new results - if (this.previousActiveHandlerDescriptor !== handlerDescriptor) { - this.clearModel(); - } - - // Receive Results from Handler and apply - return resolvedHandler.getResults(value, token).then(result => { - if (!token.isCancellationRequested) { - if (!result || !result.entries.length) { - const model = new QuickOpenModel([new PlaceholderQuickOpenEntry(resolvedHandler.getEmptyLabel(value))]); - this.showModel(model, resolvedHandler.getAutoFocus(value, { model, quickNavigateConfiguration: this.quickOpenWidget.getQuickNavigateConfiguration() }), types.withNullAsUndefined(resolvedHandler.getAriaLabel())); - } else { - this.showModel(result, resolvedHandler.getAutoFocus(value, { model: result, quickNavigateConfiguration: this.quickOpenWidget.getQuickNavigateConfiguration() }), types.withNullAsUndefined(resolvedHandler.getAriaLabel())); - } - } - }); - }); + } } private showModel(model: IModel, autoFocus?: IAutoFocus, ariaLabel?: string): void { @@ -837,7 +828,7 @@ export class RemoveFromEditorHistoryAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { interface IHistoryPickEntry extends IQuickPickItem { input: IEditorInput | IResourceInput; } @@ -854,11 +845,10 @@ export class RemoveFromEditorHistoryAction extends Action { }; }); - return this.quickInputService.pick(picks, { placeHolder: nls.localize('pickHistory', "Select an editor entry to remove from history"), matchOnDescription: true }).then(pick => { - if (pick) { - this.historyService.remove(pick.input); - } - }); + const pick = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickHistory', "Select an editor entry to remove from history"), matchOnDescription: true }); + if (pick) { + this.historyService.remove(pick.input); + } } } diff --git a/src/vs/workbench/browser/parts/quickopen/quickopen.ts b/src/vs/workbench/browser/parts/quickopen/quickopen.ts index 0589273d31..2b0ee96e15 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickopen.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickopen.ts @@ -23,12 +23,10 @@ export const QUICKOPEN_ACION_LABEL = nls.localize('quickOpen', "Go to File..."); CommandsRegistry.registerCommand({ id: QUICKOPEN_ACTION_ID, - handler: function (accessor: ServicesAccessor, prefix: string | null = null) { + handler: async function (accessor: ServicesAccessor, prefix: string | null = null) { const quickOpenService = accessor.get(IQuickOpenService); - return quickOpenService.show(typeof prefix === 'string' ? prefix : undefined).then(() => { - return undefined; - }); + await quickOpenService.show(typeof prefix === 'string' ? prefix : undefined); }, description: { description: `Quick open`, @@ -42,12 +40,10 @@ CommandsRegistry.registerCommand({ }); export const QUICKOPEN_FOCUS_SECONDARY_ACTION_ID = 'workbench.action.quickOpenPreviousEditor'; -CommandsRegistry.registerCommand(QUICKOPEN_FOCUS_SECONDARY_ACTION_ID, function (accessor: ServicesAccessor, prefix: string | null = null) { +CommandsRegistry.registerCommand(QUICKOPEN_FOCUS_SECONDARY_ACTION_ID, async function (accessor: ServicesAccessor, prefix: string | null = null) { const quickOpenService = accessor.get(IQuickOpenService); - return quickOpenService.show(undefined, { autoFocus: { autoFocusSecondEntry: true } }).then(() => { - return undefined; - }); + await quickOpenService.show(undefined, { autoFocus: { autoFocusSecondEntry: true } }); }); export class BaseQuickOpenNavigateAction extends Action { diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 75f4afa9f9..ed5644728e 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -189,19 +189,18 @@ export class SidebarPart extends CompositePart implements IViewletServi this.hideActiveComposite(); } - openViewlet(id: string | undefined, focus?: boolean): Promise { + async openViewlet(id: string | undefined, focus?: boolean): Promise { if (typeof id === 'string' && this.getViewlet(id)) { return Promise.resolve(this.doOpenViewlet(id, focus)); } - return this.extensionService.whenInstalledExtensionsRegistered() - .then(() => { - if (typeof id === 'string' && this.getViewlet(id)) { - return this.doOpenViewlet(id, focus); - } + await this.extensionService.whenInstalledExtensionsRegistered(); - return null; - }); + if (typeof id === 'string' && this.getViewlet(id)) { + return this.doOpenViewlet(id, focus); + } + + return null; } getViewlets(): ViewletDescriptor[] { diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 53559bb5b0..b54df672bc 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -382,7 +382,7 @@ class StatusBarEntryItem extends Disposable { } } - private executeCommand(id: string, args?: unknown[]) { + private async executeCommand(id: string, args?: unknown[]): Promise { args = args || []; // Maintain old behaviour of always focusing the editor here @@ -398,7 +398,11 @@ class StatusBarEntryItem extends Disposable { } */ this.telemetryService.publicLog('workbenchActionExecuted', { id, from: 'status bar' }); - this.commandService.executeCommand(id, ...args).then(undefined, err => this.notificationService.error(toErrorMessage(err))); + try { + await this.commandService.executeCommand(id, ...args); + } catch (error) { + this.notificationService.error(toErrorMessage(error)); + } } dispose(): void { diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 8e8d7805cd..388d3279ba 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -391,14 +391,13 @@ export class TitlebarPart extends Part implements ITitleService { const restoreIconContainer = append(this.windowControls, $('div.window-icon-bg')); this.maxRestoreControl = append(restoreIconContainer, $('div.window-icon')); addClass(this.maxRestoreControl, 'window-max-restore'); - this._register(addDisposableListener(this.maxRestoreControl, EventType.CLICK, e => { - this.windowService.isMaximized().then((maximized) => { - if (maximized) { - return this.windowService.unmaximizeWindow(); - } + this._register(addDisposableListener(this.maxRestoreControl, EventType.CLICK, async e => { + const maximized = await this.windowService.isMaximized(); + if (maximized) { + return this.windowService.unmaximizeWindow(); + } - return this.windowService.maximizeWindow(); - }); + return this.windowService.maximizeWindow(); })); // Close diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index b24b4e9152..0d23af2842 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -251,15 +251,13 @@ export class CustomTreeView extends Disposable implements ITreeView { set dataProvider(dataProvider: ITreeViewDataProvider | null) { if (dataProvider) { this._dataProvider = new class implements ITreeViewDataProvider { - getChildren(node: ITreeItem): Promise { + async getChildren(node: ITreeItem): Promise { if (node && node.children) { return Promise.resolve(node.children); } - const promise = node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node); - return promise.then(children => { - node.children = children; - return children; - }); + const children = await (node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node)); + node.children = children; + return children; } }; this.updateMessage(); @@ -524,19 +522,16 @@ export class CustomTreeView extends Disposable implements ITreeView { } private refreshing: boolean = false; - private doRefresh(elements: ITreeItem[]): Promise { + private async doRefresh(elements: ITreeItem[]): Promise { if (this.tree) { this.refreshing = true; - return Promise.all(elements.map(e => this.tree.refresh(e))) - .then(() => { - this.refreshing = false; - this.updateContentAreas(); - if (this.focused) { - this.focus(); - } - }); + await Promise.all(elements.map(e => this.tree.refresh(e))); + this.refreshing = false; + this.updateContentAreas(); + if (this.focused) { + this.focus(); + } } - return Promise.resolve(undefined); } private updateContentAreas(): void { diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index 99dbcf0bd7..9e5a11312b 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -618,21 +618,19 @@ export class ViewsService extends Disposable implements IViewsService { return viewDescriptorCollectionItem ? viewDescriptorCollectionItem.viewDescriptorCollection : null; } - openView(id: string, focus: boolean): Promise { + async openView(id: string, focus: boolean): Promise { const viewContainer = Registry.as(ViewExtensions.ViewsRegistry).getViewContainer(id); if (viewContainer) { const viewletDescriptor = this.viewletService.getViewlet(viewContainer.id); if (viewletDescriptor) { - return this.viewletService.openViewlet(viewletDescriptor.id, focus) - .then((viewlet: IViewsViewlet) => { - if (viewlet && viewlet.openView) { - return viewlet.openView(id, focus); - } - return null; - }); + const viewlet = await this.viewletService.openViewlet(viewletDescriptor.id, focus) as IViewsViewlet | null; + if (viewlet && viewlet.openView) { + return viewlet.openView(id, focus); + } } } - return Promise.resolve(null); + + return null; } private onDidRegisterViewContainer(viewContainer: ViewContainer): void { @@ -669,7 +667,7 @@ export class ViewsService extends Disposable implements IViewsService { }; const when = ContextKeyExpr.has(`${viewDescriptor.id}.active`); - disposables.push(CommandsRegistry.registerCommand(command.id, () => this.openView(viewDescriptor.id, true).then(() => null))); + disposables.push(CommandsRegistry.registerCommand(command.id, () => this.openView(viewDescriptor.id, true))); disposables.push(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command, diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 547e522af5..76280b37f0 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -26,28 +26,27 @@ class CodeRendererMain extends Disposable { private workbench: Workbench; - open(): Promise { + async open(): Promise { const services = this.initServices(); - return domContentLoaded().then(() => { - mark('willStartWorkbench'); + await domContentLoaded(); + mark('willStartWorkbench'); - // Create Workbench - this.workbench = new Workbench( - document.body, - services.serviceCollection, - services.logService - ); + // Create Workbench + this.workbench = new Workbench( + document.body, + services.serviceCollection, + services.logService + ); - // Layout - this._register(addDisposableListener(window, EventType.RESIZE, () => this.workbench.layout())); + // Layout + this._register(addDisposableListener(window, EventType.RESIZE, () => this.workbench.layout())); - // Workbench Lifecycle - this._register(this.workbench.onShutdown(() => this.dispose())); + // Workbench Lifecycle + this._register(this.workbench.onShutdown(() => this.dispose())); - // Startup - this.workbench.startup(); - }); + // Startup + this.workbench.startup(); } private initServices(): { serviceCollection: ServiceCollection, logService: ILogService } { diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index c12956bb2a..fd7bcdb15d 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -131,7 +131,7 @@ export class Workbench extends Layout { // Services const instantiationService = this.initServices(this.serviceCollection); - instantiationService.invokeFunction(accessor => { + instantiationService.invokeFunction(async accessor => { const lifecycleService = accessor.get(ILifecycleService); const storageService = accessor.get(IStorageService); const configurationService = accessor.get(IConfigurationService); @@ -158,7 +158,11 @@ export class Workbench extends Layout { this.layout(); // Restore - this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService).then(undefined, error => onUnexpectedError(error)); + try { + await this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService); + } catch (error) { + onUnexpectedError(error); + } }); return instantiationService; @@ -338,7 +342,7 @@ export class Workbench extends Layout { registerNotificationCommands(notificationsCenter, notificationsToasts); } - private restoreWorkbench( + private async restoreWorkbench( editorService: IEditorService, editorGroupService: IEditorGroupsService, viewletService: IViewletService, @@ -349,36 +353,39 @@ export class Workbench extends Layout { const restorePromises: Promise[] = []; // Restore editors - mark('willRestoreEditors'); - restorePromises.push(editorGroupService.whenRestored.then(() => { + restorePromises.push((async () => { + mark('willRestoreEditors'); - function openEditors(editors: IResourceEditor[], editorService: IEditorService) { - if (editors.length) { - return editorService.openEditors(editors); - } - - return Promise.resolve(undefined); - } + // first ensure the editor part is restored + await editorGroupService.whenRestored; + // then see for editors to open as instructed + let editors: IResourceEditor[]; if (Array.isArray(this.state.editor.editorsToOpen)) { - return openEditors(this.state.editor.editorsToOpen, editorService); + editors = this.state.editor.editorsToOpen; + } else { + editors = await this.state.editor.editorsToOpen; } - return this.state.editor.editorsToOpen.then(editors => openEditors(editors, editorService)); - }).then(() => mark('didRestoreEditors'))); + if (editors.length) { + await editorService.openEditors(editors); + } + + mark('didRestoreEditors'); + })()); // Restore Sidebar if (this.state.sideBar.viewletToRestore) { - mark('willRestoreViewlet'); - restorePromises.push(viewletService.openViewlet(this.state.sideBar.viewletToRestore) - .then(viewlet => { - if (!viewlet) { - return viewletService.openViewlet(viewletService.getDefaultViewletId()); // fallback to default viewlet as needed - } + restorePromises.push((async () => { + mark('willRestoreViewlet'); - return viewlet; - }) - .then(() => mark('didRestoreViewlet'))); + const viewlet = await viewletService.openViewlet(this.state.sideBar.viewletToRestore); + if (!viewlet) { + await viewletService.openViewlet(viewletService.getDefaultViewletId()); // fallback to default viewlet as needed + } + + mark('didRestoreViewlet'); + })()); } // Restore Panel @@ -401,23 +408,24 @@ export class Workbench extends Layout { // Emit a warning after 10s if restore does not complete const restoreTimeoutHandle = setTimeout(() => logService.warn('Workbench did not finish loading in 10 seconds, that might be a problem that should be reported.'), 10000); - return Promise.all(restorePromises) - .then(() => clearTimeout(restoreTimeoutHandle)) - .catch(error => onUnexpectedError(error)) - .finally(() => { + try { + await Promise.all(restorePromises); - // Set lifecycle phase to `Restored` - lifecycleService.phase = LifecyclePhase.Restored; + clearTimeout(restoreTimeoutHandle); + } catch (error) { + onUnexpectedError(error); + } finally { - // Set lifecycle phase to `Eventually` after a short delay and when idle (min 2.5sec, max 5sec) - setTimeout(() => { - this._register(runWhenIdle(() => { - lifecycleService.phase = LifecyclePhase.Eventually; - }, 2500)); - }, 2500); + // Set lifecycle phase to `Restored` + lifecycleService.phase = LifecyclePhase.Restored; - // Telemetry: startup metrics - mark('didStartWorkbench'); - }); + // Set lifecycle phase to `Eventually` after a short delay and when idle (min 2.5sec, max 5sec) + setTimeout(() => { + this._register(runWhenIdle(() => lifecycleService.phase = LifecyclePhase.Eventually, 2500)); + }, 2500); + + // Telemetry: startup metrics + mark('didStartWorkbench'); + } } } diff --git a/src/vs/workbench/common/actions.ts b/src/vs/workbench/common/actions.ts index dc2cabc3b1..14b786ff74 100644 --- a/src/vs/workbench/common/actions.ts +++ b/src/vs/workbench/common/actions.ts @@ -82,46 +82,40 @@ Registry.add(Extensions.WorkbenchActions, new class implements IWorkbenchActionR } private createCommandHandler(descriptor: SyncActionDescriptor): ICommandHandler { - return (accessor, args) => { + return async (accessor, args) => { const notificationService = accessor.get(INotificationService); const instantiationService = accessor.get(IInstantiationService); const lifecycleService = accessor.get(ILifecycleService); - Promise.resolve(this.triggerAndDisposeAction(instantiationService, lifecycleService, descriptor, args)).then(undefined, err => { - notificationService.error(err); - }); + try { + await this.triggerAndDisposeAction(instantiationService, lifecycleService, descriptor, args); + } catch (error) { + notificationService.error(error); + } }; } - private triggerAndDisposeAction(instantiationService: IInstantiationService, lifecycleService: ILifecycleService, descriptor: SyncActionDescriptor, args: any): Promise { + private async triggerAndDisposeAction(instantiationService: IInstantiationService, lifecycleService: ILifecycleService, descriptor: SyncActionDescriptor, args: any): Promise { // run action when workbench is created - return lifecycleService.when(LifecyclePhase.Ready).then(() => { - const actionInstance = instantiationService.createInstance(descriptor.syncDescriptor); - try { - actionInstance.label = descriptor.label || actionInstance.label; + await lifecycleService.when(LifecyclePhase.Ready); - // don't run the action when not enabled - if (!actionInstance.enabled) { - actionInstance.dispose(); + const actionInstance = instantiationService.createInstance(descriptor.syncDescriptor); + actionInstance.label = descriptor.label || actionInstance.label; - return undefined; - } + // don't run the action when not enabled + if (!actionInstance.enabled) { + actionInstance.dispose(); - const from = args && args.from || 'keybinding'; + return; + } - return Promise.resolve(actionInstance.run(undefined, { from })).then(() => { - actionInstance.dispose(); - }, err => { - actionInstance.dispose(); - - return Promise.reject(err); - }); - } catch (err) { - actionInstance.dispose(); - - return Promise.reject(err); - } - }); + // otherwise run and dispose + try { + const from = args && args.from || 'keybinding'; + await actionInstance.run(undefined, { from }); + } finally { + actionInstance.dispose(); + } } }); diff --git a/src/vs/workbench/common/contributions.ts b/src/vs/workbench/common/contributions.ts index 20bc5d7e0e..f5ba05cb55 100644 --- a/src/vs/workbench/common/contributions.ts +++ b/src/vs/workbench/common/contributions.ts @@ -80,9 +80,7 @@ class WorkbenchContributionsRegistry implements IWorkbenchContributionsRegistry // Otherwise wait for phase to be reached else { - lifecycleService.when(phase).then(() => { - this.doInstantiateByPhase(instantiationService, phase); - }); + lifecycleService.when(phase).then(() => this.doInstantiateByPhase(instantiationService, phase)); } } diff --git a/src/vs/workbench/common/editor/binaryEditorModel.ts b/src/vs/workbench/common/editor/binaryEditorModel.ts index 5a36e81554..e139c27d59 100644 --- a/src/vs/workbench/common/editor/binaryEditorModel.ts +++ b/src/vs/workbench/common/editor/binaryEditorModel.ts @@ -72,20 +72,17 @@ export class BinaryEditorModel extends EditorModel { return this.etag; } - load(): Promise { + async load(): Promise { // Make sure to resolve up to date stat for file resources if (this.fileService.canHandleResource(this.resource)) { - return this.fileService.resolve(this.resource, { resolveMetadata: true }).then(stat => { - this.etag = stat.etag; - if (typeof stat.size === 'number') { - this.size = stat.size; - } - - return this; - }); + const stat = await this.fileService.resolve(this.resource, { resolveMetadata: true }); + this.etag = stat.etag; + if (typeof stat.size === 'number') { + this.size = stat.size; + } } - return Promise.resolve(this); + return this; } } diff --git a/src/vs/workbench/common/editor/diffEditorInput.ts b/src/vs/workbench/common/editor/diffEditorInput.ts index 18951764ac..825936ebe7 100644 --- a/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/src/vs/workbench/common/editor/diffEditorInput.ts @@ -34,45 +34,44 @@ export class DiffEditorInput extends SideBySideEditorInput { return this.master; } - resolve(): Promise { + async resolve(): Promise { // Create Model - we never reuse our cached model if refresh is true because we cannot // decide for the inputs within if the cached model can be reused or not. There may be // inputs that need to be loaded again and thus we always recreate the model and dispose // the previous one - if any. - return this.createModel().then(resolvedModel => { - if (this.cachedModel) { - this.cachedModel.dispose(); - } + const resolvedModel = await this.createModel(); + if (this.cachedModel) { + this.cachedModel.dispose(); + } - this.cachedModel = resolvedModel; + this.cachedModel = resolvedModel; - return this.cachedModel; - }); + return this.cachedModel; } getPreferredEditorId(candidates: string[]): string { return this.forceOpenAsBinary ? BINARY_DIFF_EDITOR_ID : TEXT_DIFF_EDITOR_ID; } - private createModel(): Promise { + private async createModel(): Promise { // Join resolve call over two inputs and build diff editor model - return Promise.all([ + const models = await Promise.all([ this.originalInput.resolve(), this.modifiedInput.resolve() - ]).then(models => { - const originalEditorModel = models[0]; - const modifiedEditorModel = models[1]; + ]); - // If both are text models, return textdiffeditor model - if (modifiedEditorModel instanceof BaseTextEditorModel && originalEditorModel instanceof BaseTextEditorModel) { - return new TextDiffEditorModel(originalEditorModel, modifiedEditorModel); - } + const originalEditorModel = models[0]; + const modifiedEditorModel = models[1]; - // Otherwise return normal diff model - return new DiffEditorModel(originalEditorModel, modifiedEditorModel); - }); + // If both are text models, return textdiffeditor model + if (modifiedEditorModel instanceof BaseTextEditorModel && originalEditorModel instanceof BaseTextEditorModel) { + return new TextDiffEditorModel(originalEditorModel, modifiedEditorModel); + } + + // Otherwise return normal diff model + return new DiffEditorModel(originalEditorModel, modifiedEditorModel); } dispose(): void { diff --git a/src/vs/workbench/common/editor/diffEditorModel.ts b/src/vs/workbench/common/editor/diffEditorModel.ts index 4154668947..53e73c1501 100644 --- a/src/vs/workbench/common/editor/diffEditorModel.ts +++ b/src/vs/workbench/common/editor/diffEditorModel.ts @@ -37,11 +37,13 @@ export class DiffEditorModel extends EditorModel { return this._modifiedModel; } - load(): Promise { - return Promise.all([ + async load(): Promise { + await Promise.all([ this._originalModel ? this._originalModel.load() : Promise.resolve(undefined), this._modifiedModel ? this._modifiedModel.load() : Promise.resolve(undefined), - ]).then(() => this); + ]); + + return this; } isResolved(): boolean { diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 7a417fbaef..25ef75d54f 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -76,31 +76,31 @@ export class ResourceEditorInput extends EditorInput implements IModeSupport { this.preferredMode = mode; } - resolve(): Promise { + async resolve(): Promise { if (!this.modelReference) { this.modelReference = this.textModelResolverService.createModelReference(this.resource); } - return this.modelReference.then(ref => { - const model = ref.object; + const ref = await this.modelReference; - // Ensure the resolved model is of expected type - if (!(model instanceof ResourceEditorModel)) { - ref.dispose(); - this.modelReference = null; + const model = ref.object; - return Promise.reject(new Error(`Unexpected model for ResourceInput: ${this.resource}`)); - } + // Ensure the resolved model is of expected type + if (!(model instanceof ResourceEditorModel)) { + ref.dispose(); + this.modelReference = null; - this.cachedModel = model; + return Promise.reject(new Error(`Unexpected model for ResourceInput: ${this.resource}`)); + } - // Set mode if we have a preferred mode configured - if (this.preferredMode) { - model.setMode(this.preferredMode); - } + this.cachedModel = model; - return model; - }); + // Set mode if we have a preferred mode configured + if (this.preferredMode) { + model.setMode(this.preferredMode); + } + + return model; } matches(otherInput: unknown): boolean { diff --git a/src/vs/workbench/common/editor/resourceEditorModel.ts b/src/vs/workbench/common/editor/resourceEditorModel.ts index da3dae36f3..d9a383d536 100644 --- a/src/vs/workbench/common/editor/resourceEditorModel.ts +++ b/src/vs/workbench/common/editor/resourceEditorModel.ts @@ -26,6 +26,7 @@ export class ResourceEditorModel extends BaseTextEditorModel { } dispose(): void { + // TODO@Joao: force this class to dispose the underlying model if (this.textEditorModelHandle) { this.modelService.destroyModel(this.textEditorModelHandle); diff --git a/src/vs/workbench/common/editor/textDiffEditorModel.ts b/src/vs/workbench/common/editor/textDiffEditorModel.ts index 16b266c9fd..b24a623d42 100644 --- a/src/vs/workbench/common/editor/textDiffEditorModel.ts +++ b/src/vs/workbench/common/editor/textDiffEditorModel.ts @@ -33,12 +33,12 @@ export class TextDiffEditorModel extends DiffEditorModel { return this._modifiedModel; } - load(): Promise { - return super.load().then(() => { - this.updateTextDiffEditorModel(); + async load(): Promise { + await super.load(); - return this; - }); + this.updateTextDiffEditorModel(); + + return this; } private updateTextDiffEditorModel(): void { diff --git a/src/vs/workbench/common/editor/untitledEditorModel.ts b/src/vs/workbench/common/editor/untitledEditorModel.ts index abbc289baa..94e0a58036 100644 --- a/src/vs/workbench/common/editor/untitledEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledEditorModel.ts @@ -11,7 +11,7 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { Event, Emitter } from 'vs/base/common/event'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; import { ITextBufferFactory } from 'vs/editor/common/model'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; @@ -135,52 +135,48 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin return Promise.resolve(); } - load(): Promise { + async load(): Promise { // Check for backups first - return this.backupFileService.loadBackupResource(this.resource).then(backupResource => { - if (backupResource) { - return this.backupFileService.resolveBackupContent(backupResource); - } + let backup: IResolvedBackup | undefined = undefined; + const backupResource = await this.backupFileService.loadBackupResource(this.resource); + if (backupResource) { + backup = await this.backupFileService.resolveBackupContent(backupResource); + } - return Promise.resolve(undefined); - }).then(backup => { - const hasBackup = !!backup; + // untitled associated to file path are dirty right away as well as untitled with content + this.setDirty(this._hasAssociatedFilePath || !!backup || !!this.initialValue); - // untitled associated to file path are dirty right away as well as untitled with content - this.setDirty(this._hasAssociatedFilePath || hasBackup || !!this.initialValue); + let untitledContents: ITextBufferFactory; + if (backup) { + untitledContents = backup.value; + } else { + untitledContents = createTextBufferFactory(this.initialValue || ''); + } - let untitledContents: ITextBufferFactory; - if (backup) { - untitledContents = backup.value; - } else { - untitledContents = createTextBufferFactory(this.initialValue || ''); - } + // Create text editor model if not yet done + if (!this.textEditorModel) { + this.createTextEditorModel(untitledContents, this.resource, this.preferredMode); + } - // Create text editor model if not yet done - if (!this.textEditorModel) { - this.createTextEditorModel(untitledContents, this.resource, this.preferredMode); - } + // Otherwise update + else { + this.updateTextEditorModel(untitledContents, this.preferredMode); + } - // Otherwise update - else { - this.updateTextEditorModel(untitledContents, this.preferredMode); - } + // Encoding + this.configuredEncoding = this.configurationService.getValue(this.resource, 'files.encoding'); - // Encoding - this.configuredEncoding = this.configurationService.getValue(this.resource, 'files.encoding'); + // We know for a fact there is a text editor model here + const textEditorModel = this.textEditorModel!; - // We know for a fact there is a text editor model here - const textEditorModel = this.textEditorModel!; + // Listen to content changes + this._register(textEditorModel.onDidChangeContent(() => this.onModelContentChanged())); - // Listen to content changes - this._register(textEditorModel.onDidChangeContent(() => this.onModelContentChanged())); + // Listen to mode changes + this._register(textEditorModel.onDidChangeLanguage(() => this.onConfigurationChange())); // mode change can have impact on config - // Listen to mode changes - this._register(textEditorModel.onDidChangeLanguage(() => this.onConfigurationChange())); // mode change can have impact on config - - return this as UntitledEditorModel & IResolvedTextEditorModel; - }); + return this as UntitledEditorModel & IResolvedTextEditorModel; } private onModelContentChanged(): void { diff --git a/src/vs/workbench/contrib/backup/common/backupRestorer.ts b/src/vs/workbench/contrib/backup/common/backupRestorer.ts index 7fc056650e..40158dc9e3 100644 --- a/src/vs/workbench/contrib/backup/common/backupRestorer.ts +++ b/src/vs/workbench/contrib/backup/common/backupRestorer.ts @@ -33,46 +33,47 @@ export class BackupRestorer implements IWorkbenchContribution { this.lifecycleService.when(LifecyclePhase.Restored).then(() => this.doRestoreBackups()); } - private doRestoreBackups(): Promise { + private async doRestoreBackups(): Promise { // Find all files and untitled with backups - return this.backupFileService.getWorkspaceFileBackups().then(backups => { + const backups = await this.backupFileService.getWorkspaceFileBackups(); + const unresolvedBackups = await this.doResolveOpenedBackups(backups); - // Resolve backups that are opened - return this.doResolveOpenedBackups(backups).then((unresolved): Promise | undefined => { + // Some failed to restore or were not opened at all so we open and resolve them manually + if (unresolvedBackups.length > 0) { + await this.doOpenEditors(unresolvedBackups); - // Some failed to restore or were not opened at all so we open and resolve them manually - if (unresolved.length > 0) { - return this.doOpenEditors(unresolved).then(() => this.doResolveOpenedBackups(unresolved)); - } + return this.doResolveOpenedBackups(unresolvedBackups); + } - return undefined; - }); - }); + return undefined; } - private doResolveOpenedBackups(backups: URI[]): Promise { - const restorePromises: Promise[] = []; - const unresolved: URI[] = []; + private async doResolveOpenedBackups(backups: URI[]): Promise { + const unresolvedBackups: URI[] = []; - backups.forEach(backup => { + await Promise.all(backups.map(async backup => { const openedEditor = this.editorService.getOpened({ resource: backup }); if (openedEditor) { - restorePromises.push(openedEditor.resolve().then(undefined, () => unresolved.push(backup))); + try { + await openedEditor.resolve(); // trigger load + } catch (error) { + unresolvedBackups.push(backup); // ignore error and remember as unresolved + } } else { - unresolved.push(backup); + unresolvedBackups.push(backup); } - }); + })); - return Promise.all(restorePromises).then(() => unresolved, () => unresolved); + return unresolvedBackups; } - private doOpenEditors(resources: URI[]): Promise { + private async doOpenEditors(resources: URI[]): Promise { const hasOpenedEditors = this.editorService.visibleEditors.length > 0; const inputs = resources.map((resource, index) => this.resolveInput(resource, index, hasOpenedEditors)); // Open all remaining backups as editors and resolve them to load their backups - return this.editorService.openEditors(inputs).then(() => undefined); + await this.editorService.openEditors(inputs); } private resolveInput(resource: URI, index: number, hasOpenedEditors: boolean): IResourceInput | IUntitledResourceInput { diff --git a/src/vs/workbench/contrib/cli/node/cli.contribution.ts b/src/vs/workbench/contrib/cli/node/cli.contribution.ts index e187bec14a..e28d40a9ee 100644 --- a/src/vs/workbench/contrib/cli/node/cli.contribution.ts +++ b/src/vs/workbench/contrib/cli/node/cli.contribution.ts @@ -7,6 +7,7 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; import * as cp from 'child_process'; import * as pfs from 'vs/base/node/pfs'; +import * as extpath from 'vs/base/node/extpath'; import * as platform from 'vs/base/common/platform'; import { promisify } from 'util'; import { Action } from 'vs/base/common/actions'; @@ -91,7 +92,7 @@ class InstallAction extends Action { private isInstalled(): Promise { return pfs.lstat(this.target) .then(stat => stat.isSymbolicLink()) - .then(() => pfs.readlink(this.target)) + .then(() => extpath.realpath(this.target)) .then(link => link === getSource()) .then(undefined, ignore('ENOENT', false)); } diff --git a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts new file mode 100644 index 0000000000..28d508fe79 --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { IAction } from 'vs/base/common/actions'; +import { Disposable, dispose } from 'vs/base/common/lifecycle'; +import { IMenu } from 'vs/platform/actions/common/actions'; +import { attachButtonStyler } from 'vs/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +export class CommentFormActions extends Disposable { + private _buttonElements: HTMLElement[] = []; + + constructor( + private container: HTMLElement, + private actionHandler: (action: IAction) => void, + private themeService: IThemeService + ) { + super(); + } + + setActions(menu: IMenu) { + dispose(this._toDispose); + this._buttonElements.forEach(b => DOM.removeNode(b)); + + const groups = menu.getActions({ shouldForwardArgs: true }); + for (const group of groups) { + const [, actions] = group; + + actions.forEach(action => { + const button = new Button(this.container); + this._buttonElements.push(button.element); + + this._toDispose.push(button); + this._toDispose.push(attachButtonStyler(button, this.themeService)); + this._toDispose.push(button.onDidClick(() => this.actionHandler(action))); + + button.enabled = action.enabled; + button.label = action.label; + }); + } + } +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/comments/browser/commentMenus.ts b/src/vs/workbench/contrib/comments/browser/commentMenus.ts new file mode 100644 index 0000000000..9dcacebc59 --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentMenus.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; +import { IAction } from 'vs/base/common/actions'; +import { MainThreadCommentController } from 'vs/workbench/api/browser/mainThreadComments'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { Comment, CommentThread2 } from 'vs/editor/common/modes'; +import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; + +export class CommentMenus implements IDisposable { + constructor( + controller: MainThreadCommentController, + @IContextKeyService private contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, + @IContextMenuService private readonly contextMenuService: IContextMenuService + ) { + const commentControllerKey = this.contextKeyService.createKey('commentController', undefined); + + commentControllerKey.set(controller.contextValue); + } + + getCommentThreadTitleActions(commentThread: CommentThread2, contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.CommentThreadTitle, contextKeyService); + } + + getCommentThreadActions(commentThread: CommentThread2, contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.CommentThreadActions, contextKeyService); + } + + getCommentTitleActions(comment: Comment, contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.CommentTitle, contextKeyService); + } + + getCommentActions(comment: Comment, contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.CommentActions, contextKeyService); + } + + private getMenu(menuId: MenuId, contextKeyService: IContextKeyService): IMenu { + const menu = this.menuService.createMenu(menuId, contextKeyService); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + fillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => true); + + return menu; + } + + dispose(): void { + + } +} diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 515b57629a..bd686dd3cb 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -8,7 +8,7 @@ import * as dom from 'vs/base/browser/dom'; import * as modes from 'vs/editor/common/modes'; import { ActionsOrientation, ActionViewItem, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button } from 'vs/base/browser/ui/button/button'; -import { Action, IActionRunner } from 'vs/base/common/actions'; +import { Action, IActionRunner, IAction } from 'vs/base/common/actions'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; @@ -35,6 +35,11 @@ import { ToggleReactionsAction, ReactionAction, ReactionActionViewItem } from '. import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; +import { MenuItemAction } from 'vs/platform/actions/common/actions'; +import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; const UPDATE_COMMENT_LABEL = nls.localize('label.updateComment', "Update comment"); const UPDATE_IN_PROGRESS_LABEL = nls.localize('label.updatingComment', "Updating comment..."); @@ -57,10 +62,13 @@ export class CommentNode extends Disposable { private _updateCommentButton: Button; private _errorEditingContainer: HTMLElement; private _isPendingLabel: HTMLElement; + private _contextKeyService: IContextKeyService; + private _commentContextValue: IContextKey; private _deleteAction: Action; protected actionRunner?: IActionRunner; protected toolbar: ToolBar; + private _commentFormActions: CommentFormActions; private _onDidDelete = new Emitter(); @@ -85,12 +93,17 @@ export class CommentNode extends Disposable { @IModelService private modelService: IModelService, @IModeService private modeService: IModeService, @IDialogService private dialogService: IDialogService, + @IKeybindingService private keybindingService: IKeybindingService, @INotificationService private notificationService: INotificationService, - @IContextMenuService private contextMenuService: IContextMenuService + @IContextMenuService private contextMenuService: IContextMenuService, + @IContextKeyService contextKeyService: IContextKeyService ) { super(); this._domNode = dom.$('div.review-comment'); + this._contextKeyService = contextKeyService.createScoped(this._domNode); + this._commentContextValue = this._contextKeyService.createKey('comment', comment.contextValue); + this._domNode.tabIndex = 0; const avatar = dom.append(this._domNode, dom.$('div.avatar-container')); if (comment.userIconPath) { @@ -139,7 +152,7 @@ export class CommentNode extends Disposable { } private createActionsToolbar() { - const actions: Action[] = []; + const actions: IAction[] = []; let reactionGroup = this.commentService.getReactionGroup(this.owner); if (reactionGroup && reactionGroup.length) { @@ -163,6 +176,17 @@ export class CommentNode extends Disposable { actions.push(this._deleteAction); } + let commentMenus = this.commentService.getCommentMenus(this.owner); + const menu = commentMenus.getCommentTitleActions(this.comment, this._contextKeyService); + this._toDispose.push(menu); + this._toDispose.push(menu.onDidChange(e => { + const contributedActions = menu.getActions({ shouldForwardArgs: true }).reduce((r, [, actions]) => [...r, ...actions], []); + this.toolbar.setActions(contributedActions); + })); + + const contributedActions = menu.getActions({ shouldForwardArgs: true }).reduce((r, [, actions]) => [...r, ...actions], []); + actions.push(...contributedActions); + if (actions.length) { this.toolbar = new ToolBar(this._actionsToolbarContainer, this.contextMenuService, { actionViewItemProvider: action => { @@ -185,6 +209,12 @@ export class CommentNode extends Disposable { orientation: ActionsOrientation.HORIZONTAL }); + this.toolbar.context = { + thread: this.commentThread, + commentUniqueId: this.comment.uniqueIdInThread, + $mid: 9 + }; + this.registerActionBarListeners(this._actionsToolbarContainer); this.toolbar.setActions(actions, [])(); this._toDispose.push(this.toolbar); @@ -196,12 +226,15 @@ export class CommentNode extends Disposable { if (action.id === 'comment.delete' || action.id === 'comment.edit' || action.id === ToggleReactionsAction.ID) { options = { label: false, icon: true }; } else { - options = { label: true, icon: true }; + options = { label: false, icon: true }; } if (action.id === ReactionAction.ID) { let item = new ReactionActionViewItem(action); return item; + } else if (action instanceof MenuItemAction) { + let item = new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + return item; } else { let item = new ActionViewItem({}, action, options); return item; @@ -391,14 +424,12 @@ export class CommentNode extends Disposable { uri: this._commentEditor.getModel()!.uri, value: this.comment.body.value }; - this.commentService.setActiveCommentThread(commentThread); this._commentEditorDisposables.push(this._commentEditor.onDidFocusEditorWidget(() => { commentThread.input = { uri: this._commentEditor!.getModel()!.uri, value: this.comment.body.value }; - this.commentService.setActiveCommentThread(commentThread); })); this._commentEditorDisposables.push(this._commentEditor.onDidChangeModelContent(e => { @@ -419,10 +450,15 @@ export class CommentNode extends Disposable { private removeCommentEditor() { this.isEditing = false; - this._editAction.enabled = true; + if (this._editAction) { + this._editAction.enabled = true; + } this._body.classList.remove('hidden'); - this._commentEditorModel.dispose(); + if (this._commentEditorModel) { + this._commentEditorModel.dispose(); + } + this._commentEditorDisposables.forEach(dispose => dispose.dispose()); this._commentEditorDisposables = []; if (this._commentEditor) { @@ -450,7 +486,6 @@ export class CommentNode extends Disposable { uri: this._commentEditor.getModel()!.uri, value: newBody }; - this.commentService.setActiveCommentThread(commentThread); let commandId = this.comment.editCommand.id; let args = this.comment.editCommand.arguments || []; @@ -488,7 +523,6 @@ export class CommentNode extends Disposable { if (result.confirmed) { try { if (this.comment.deleteCommand) { - this.commentService.setActiveCommentThread(this.commentThread as modes.CommentThread2); let commandId = this.comment.deleteCommand.id; let args = this.comment.deleteCommand.arguments || []; @@ -512,41 +546,81 @@ export class CommentNode extends Disposable { }); } + public switchToEditMode() { + if (this.isEditing) { + return; + } + + this.isEditing = true; + this._body.classList.add('hidden'); + this._commentEditContainer = dom.append(this._commentDetailsContainer, dom.$('.edit-container')); + this.createCommentEditor(); + this._errorEditingContainer = dom.append(this._commentEditContainer, dom.$('.validation-error.hidden')); + const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions')); + + const menus = this.commentService.getCommentMenus(this.owner); + const menu = menus.getCommentActions(this.comment, this._contextKeyService); + + this._toDispose.push(menu); + this._toDispose.push(menu.onDidChange(() => { + this._commentFormActions.setActions(menu); + })); + + this._commentFormActions = new CommentFormActions(formActions, (action: IAction): void => { + let text = this._commentEditor!.getValue(); + + action.run({ + thread: this.commentThread, + commentUniqueId: this.comment.uniqueIdInThread, + text: text, + $mid: 10 + }); + + this.removeCommentEditor(); + }, this.themeService); + + this._commentFormActions.setActions(menu); + } + private createEditAction(commentDetailsContainer: HTMLElement): Action { return new Action('comment.edit', nls.localize('label.edit', "Edit"), 'octicon octicon-pencil', true, () => { - this.isEditing = true; - this._body.classList.add('hidden'); - this._commentEditContainer = dom.append(commentDetailsContainer, dom.$('.edit-container')); - this.createCommentEditor(); - - this._errorEditingContainer = dom.append(this._commentEditContainer, dom.$('.validation-error.hidden')); - const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions')); - - const cancelEditButton = new Button(formActions); - cancelEditButton.label = nls.localize('label.cancel', "Cancel"); - this._toDispose.push(attachButtonStyler(cancelEditButton, this.themeService)); - - this._toDispose.push(cancelEditButton.onDidClick(_ => { - this.removeCommentEditor(); - })); - - this._updateCommentButton = new Button(formActions); - this._updateCommentButton.label = UPDATE_COMMENT_LABEL; - this._toDispose.push(attachButtonStyler(this._updateCommentButton, this.themeService)); - - this._toDispose.push(this._updateCommentButton.onDidClick(_ => { - this.editComment(); - })); - - this._commentEditorDisposables.push(this._commentEditor!.onDidChangeModelContent(_ => { - this._updateCommentButton.enabled = !!this._commentEditor!.getValue(); - })); - - this._editAction.enabled = false; - return Promise.resolve(); + return this.editCommentAction(commentDetailsContainer); }); } + private editCommentAction(commentDetailsContainer: HTMLElement) { + this.isEditing = true; + this._body.classList.add('hidden'); + this._commentEditContainer = dom.append(commentDetailsContainer, dom.$('.edit-container')); + this.createCommentEditor(); + + this._errorEditingContainer = dom.append(this._commentEditContainer, dom.$('.validation-error.hidden')); + const formActions = dom.append(this._commentEditContainer, dom.$('.form-actions')); + + const cancelEditButton = new Button(formActions); + cancelEditButton.label = nls.localize('label.cancel', "Cancel"); + this._toDispose.push(attachButtonStyler(cancelEditButton, this.themeService)); + + this._toDispose.push(cancelEditButton.onDidClick(_ => { + this.removeCommentEditor(); + })); + + this._updateCommentButton = new Button(formActions); + this._updateCommentButton.label = UPDATE_COMMENT_LABEL; + this._toDispose.push(attachButtonStyler(this._updateCommentButton, this.themeService)); + + this._toDispose.push(this._updateCommentButton.onDidClick(_ => { + this.editComment(); + })); + + this._commentEditorDisposables.push(this._commentEditor!.onDidChangeModelContent(_ => { + this._updateCommentButton.enabled = !!this._commentEditor!.getValue(); + })); + + this._editAction.enabled = false; + return Promise.resolve(); + } + private registerActionBarListeners(actionsContainer: HTMLElement): void { this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseenter', () => { actionsContainer.classList.remove('hidden'); @@ -581,6 +655,14 @@ export class CommentNode extends Disposable { this._body.appendChild(this._md); } + if (newComment.mode !== undefined && newComment.mode !== this.comment.mode) { + if (newComment.mode === modes.CommentMode.Editing) { + this.switchToEditMode(); + } else { + this.removeCommentEditor(); + } + } + const shouldUpdateActions = newComment.editCommand !== this.comment.editCommand || newComment.deleteCommand !== this.comment.deleteCommand; this.comment = newComment; @@ -610,6 +692,12 @@ export class CommentNode extends Disposable { if (this.comment.commentReactions && this.comment.commentReactions.length) { this.createReactionsContainer(this._commentDetailsContainer); } + + if (this.comment.contextValue) { + this._commentContextValue.set(this.comment.contextValue); + } else { + this._commentContextValue.reset(); + } } focus() { diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index db34f502e7..52b3f2fe7c 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CommentThread, DocumentCommentProvider, CommentThreadChangedEvent, CommentInfo, Comment, CommentReaction, CommentingRanges, CommentThread2 } from 'vs/editor/common/modes'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -14,6 +14,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { assign } from 'vs/base/common/objects'; import { ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { MainThreadCommentController } from 'vs/workbench/api/browser/mainThreadComments'; +import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus'; export const ICommentService = createDecorator('commentService'); @@ -37,9 +38,7 @@ export interface ICommentService { readonly onDidSetResourceCommentInfos: Event; readonly onDidSetAllCommentThreads: Event; readonly onDidUpdateCommentThreads: Event; - readonly onDidChangeActiveCommentThread: Event; readonly onDidChangeActiveCommentingRange: Event<{ range: Range, commentingRangesInfo: CommentingRanges }>; - readonly onDidChangeInput: Event; readonly onDidSetDataProvider: Event; readonly onDidDeleteDataProvider: Event; setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void; @@ -47,9 +46,13 @@ export interface ICommentService { removeWorkspaceComments(owner: string): void; registerCommentController(owner: string, commentControl: MainThreadCommentController): void; unregisterCommentController(owner: string): void; + getCommentController(owner: string): MainThreadCommentController | undefined; + createCommentThreadTemplate(owner: string, resource: URI, range: Range): void; + getCommentMenus(owner: string): CommentMenus; registerDataProvider(owner: string, commentProvider: DocumentCommentProvider): void; unregisterDataProvider(owner: string): void; updateComments(ownerId: string, event: CommentThreadChangedEvent): void; + disposeCommentThread(ownerId: string, threadId: string): void; createNewCommentThread(owner: string, resource: URI, range: Range, text: string): Promise; replyToCommentThread(owner: string, resource: URI, range: Range, thread: CommentThread, text: string): Promise; editComment(owner: string, resource: URI, comment: Comment, text: string): Promise; @@ -66,9 +69,6 @@ export interface ICommentService { deleteReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise; getReactionGroup(owner: string): CommentReaction[] | undefined; toggleReaction(owner: string, resource: URI, thread: CommentThread2, comment: Comment, reaction: CommentReaction): Promise; - getCommentThreadFromTemplate(owner: string, resource: URI, range: IRange, ): CommentThread2 | undefined; - setActiveCommentThread(commentThread: CommentThread | null): void; - setInput(input: string): void; } export class CommentService extends Disposable implements ICommentService { @@ -89,11 +89,6 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidUpdateCommentThreads: Emitter = this._register(new Emitter()); readonly onDidUpdateCommentThreads: Event = this._onDidUpdateCommentThreads.event; - private readonly _onDidChangeActiveCommentThread = this._register(new Emitter()); - readonly onDidChangeActiveCommentThread: Event = this._onDidChangeActiveCommentThread.event; - - private readonly _onDidChangeInput: Emitter = this._register(new Emitter()); - readonly onDidChangeInput: Event = this._onDidChangeInput.event; private readonly _onDidChangeActiveCommentingRange: Emitter<{ range: Range, commentingRangesInfo: CommentingRanges @@ -106,19 +101,14 @@ export class CommentService extends Disposable implements ICommentService { private _commentProviders = new Map(); private _commentControls = new Map(); + private _commentMenus = new Map(); - constructor() { + constructor( + @IInstantiationService protected instantiationService: IInstantiationService + ) { super(); } - setActiveCommentThread(commentThread: CommentThread | null) { - this._onDidChangeActiveCommentThread.fire(commentThread); - } - - setInput(input: string) { - this._onDidChangeInput.fire(input); - } - setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void { this._onDidSetResourceCommentInfos.fire({ resource, commentInfos }); } @@ -141,6 +131,39 @@ export class CommentService extends Disposable implements ICommentService { this._onDidDeleteDataProvider.fire(owner); } + getCommentController(owner: string): MainThreadCommentController | undefined { + return this._commentControls.get(owner); + } + + createCommentThreadTemplate(owner: string, resource: URI, range: Range): void { + const commentController = this._commentControls.get(owner); + + if (!commentController) { + return; + } + + commentController.createCommentThreadTemplate(resource, range); + } + + disposeCommentThread(owner: string, threadId: string) { + let controller = this.getCommentController(owner); + if (controller) { + controller.deleteCommentThreadMain(threadId); + } + } + + getCommentMenus(owner: string): CommentMenus { + if (this._commentMenus.get(owner)) { + return this._commentMenus.get(owner)!; + } + + let controller = this._commentControls.get(owner); + + let menu = this.instantiationService.createInstance(CommentMenus, controller!); + this._commentMenus.set(owner, menu); + return menu; + } + registerDataProvider(owner: string, commentProvider: DocumentCommentProvider): void { this._commentProviders.set(owner, commentProvider); this._onDidSetDataProvider.fire(); @@ -256,16 +279,6 @@ export class CommentService extends Disposable implements ICommentService { } } - getCommentThreadFromTemplate(owner: string, resource: URI, range: IRange, ): CommentThread2 | undefined { - const commentController = this._commentControls.get(owner); - - if (commentController) { - return commentController.getCommentThreadFromTemplate(resource, range); - } - - return undefined; - } - getReactionGroup(owner: string): CommentReaction[] | undefined { const commentProvider = this._commentControls.get(owner); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 5e1cda93de..0a205c1027 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -5,9 +5,9 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button } from 'vs/base/browser/ui/button/button'; -import { Action } from 'vs/base/common/actions'; +import { Action, IAction } from 'vs/base/common/actions'; import * as arrays from 'vs/base/common/arrays'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; @@ -38,9 +38,18 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { generateUuid } from 'vs/base/common/uuid'; import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus'; +import { MenuItemAction, IMenu } from 'vs/platform/actions/common/actions'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; +import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; -const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-x'; +const COLLAPSE_ACTION_CLASS = 'expand-review-action octicon octicon-chevron-up'; const COMMENT_SCHEME = 'comment'; @@ -70,6 +79,11 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private _styleElement: HTMLStyleElement; private _formActions: HTMLElement | null; private _error: HTMLElement; + private _contextKeyService: IContextKeyService; + private _threadIsEmpty: IContextKey; + private _commentThreadContextValue: IContextKey; + private _commentEditorIsEmpty: IContextKey; + private _commentFormActions: CommentFormActions; public get owner(): string { return this._owner; @@ -86,6 +100,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget return this._draftMode; } + private _commentMenus: CommentMenus; + constructor( editor: ICodeEditor, private _owner: string, @@ -98,15 +114,25 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget @IModelService private modelService: IModelService, @IThemeService private themeService: IThemeService, @ICommentService private commentService: ICommentService, - @IOpenerService private openerService: IOpenerService + @IOpenerService private openerService: IOpenerService, + @IKeybindingService private keybindingService: IKeybindingService, + @INotificationService private notificationService: INotificationService, + @IContextMenuService private contextMenuService: IContextMenuService, + @IContextKeyService contextKeyService: IContextKeyService ) { super(editor, { keepEditorSelection: true }); + this._contextKeyService = contextKeyService.createScoped(this.domNode); + this._threadIsEmpty = CommentContextKeys.commentThreadIsEmpty.bindTo(this._contextKeyService); + this._threadIsEmpty.set(!_commentThread.comments || !_commentThread.comments.length); + this._commentThreadContextValue = contextKeyService.createKey('commentThread', _commentThread.contextValue); + this._resizeObserver = null; this._isExpanded = _commentThread.collapsibleState ? _commentThread.collapsibleState === modes.CommentThreadCollapsibleState.Expanded : undefined; this._globalToDispose = []; this._commentThreadDisposables = []; this._submitActionsDisposables = []; this._formActions = null; + this._commentMenus = this.commentService.getCommentMenus(this._owner); this.create(); this._styleElement = dom.createStyleSheet(this.domNode); @@ -185,10 +211,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._bodyElement = dom.$('.body'); container.appendChild(this._bodyElement); - - dom.addDisposableListener(this._bodyElement, dom.EventType.FOCUS_IN, e => { - this.commentService.setActiveCommentThread(this._commentThread); - }); } protected _fillHead(container: HTMLElement): void { @@ -198,12 +220,41 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this.createThreadLabel(); const actionsContainer = dom.append(this._headElement, dom.$('.review-actions')); - this._actionbarWidget = new ActionBar(actionsContainer, {}); + this._actionbarWidget = new ActionBar(actionsContainer, { + actionViewItemProvider: (action: IAction) => { + if (action instanceof MenuItemAction) { + let item = new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + return item; + } else { + let item = new ActionViewItem({}, action, { label: false, icon: true }); + return item; + } + } + }); + this._disposables.push(this._actionbarWidget); this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), COLLAPSE_ACTION_CLASS, true, () => this.collapse()); - this._actionbarWidget.push(this._collapseAction, { label: false, icon: true }); + if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { + const menu = this._commentMenus.getCommentThreadTitleActions(this._commentThread as modes.CommentThread2, this._contextKeyService); + this.setActionBarActions(menu); + + this._disposables.push(menu); + this._disposables.push(menu.onDidChange(e => { + this.setActionBarActions(menu); + })); + } else { + this._actionbarWidget.push([this._collapseAction], { label: false, icon: true }); + } + + this._actionbarWidget.context = this._commentThread; + } + + private setActionBarActions(menu: IMenu): void { + const groups = menu.getActions({ shouldForwardArgs: true }).reduce((r, [, actions]) => [...r, ...actions], []); + this._actionbarWidget.clear(); + this._actionbarWidget.push([...groups, this._collapseAction], { label: false, icon: true }); } public collapse(): Promise { @@ -214,9 +265,9 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } else { const deleteCommand = (this._commentThread as modes.CommentThread2).deleteCommand; if (deleteCommand) { - this.commentService.setActiveCommentThread(this._commentThread); return this.commandService.executeCommand(deleteCommand.id, ...(deleteCommand.arguments || [])); } else if (this._commentEditor.getValue() === '') { + this.commentService.disposeCommentThread(this._owner, this._commentThread.threadId!); this.dispose(); return Promise.resolve(); } @@ -245,9 +296,10 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } - async update(commentThread: modes.CommentThread | modes.CommentThread2, replaceTemplate: boolean = false) { + async update(commentThread: modes.CommentThread | modes.CommentThread2) { const oldCommentsLen = this._commentElements.length; const newCommentsLen = commentThread.comments ? commentThread.comments.length : 0; + this._threadIsEmpty.set(!newCommentsLen); let commentElementsToDel: CommentNode[] = []; let commentElementsToDelIndex: number[] = []; @@ -294,26 +346,12 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThread = commentThread; this._commentElements = newCommentNodeList; - this.createThreadLabel(replaceTemplate); - - if (replaceTemplate) { - // since we are replacing the old comment thread, we need to rebind the listeners. - this._commentThreadDisposables.forEach(global => global.dispose()); - this._commentThreadDisposables = []; - } - - if (replaceTemplate) { - this.createTextModelListener(); - } + this.createThreadLabel(); if (this._formActions && this._commentEditor.hasModel()) { dom.clearNode(this._formActions); const model = this._commentEditor.getModel(); this.createCommentWidgetActions2(this._formActions, model); - - if (replaceTemplate) { - this.createCommentWidgetActionsListener(this._formActions, model); - } } // Move comment glyph widget and show position if the line has changed. @@ -346,6 +384,12 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this.hide(); } } + + if (this._commentThread.contextValue) { + this._commentThreadContextValue.set(this._commentThread.contextValue); + } else { + this._commentThreadContextValue.reset(); + } } updateDraftMode(draftMode: modes.DraftMode | undefined) { @@ -368,7 +412,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentEditor.layout({ height: 5 * 18, width: widthInPixel - 54 /* margin 20px * 10 + scrollbar 14px*/ }); } - display(lineNumber: number, fromTemplate: boolean = false) { + display(lineNumber: number) { this._commentGlyph = new CommentGlyphWidget(this.editor, lineNumber); this._disposables.push(this.editor.onMouseDown(e => this.onEditorMouseDown(e))); @@ -394,18 +438,30 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; this._commentForm = dom.append(this._bodyElement, dom.$('.comment-form')); this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, this._commentForm, SimpleCommentEditor.getEditorOptions(), this._parentEditor, this); + this._commentEditorIsEmpty = CommentContextKeys.commentIsEmpty.bindTo(this._contextKeyService); + this._commentEditorIsEmpty.set(!this._pendingComment); const modeId = generateUuid() + '-' + (hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID); const params = JSON.stringify({ extensionId: this.extensionId, commentThreadId: this.commentThread.threadId }); - const resource = URI.parse(`${COMMENT_SCHEME}:commentinput-${modeId}.md?${params}`); + + let resource = URI.parse(`${COMMENT_SCHEME}://${this.extensionId}/commentinput-${modeId}.md?${params}`); // TODO. Remove params once extensions adopt authority. + let commentController = this.commentService.getCommentController(this.owner); + if (commentController) { + resource = resource.with({ authority: commentController.id }); + } + const model = this.modelService.createModel(this._pendingComment || '', this.modeService.createByFilepathOrFirstLine(resource.path), resource, false); this._disposables.push(model); this._commentEditor.setModel(model); this._disposables.push(this._commentEditor); - this._disposables.push(this._commentEditor.getModel()!.onDidChangeContent(() => this.setCommentEditorDecorations())); + this._disposables.push(this._commentEditor.getModel()!.onDidChangeContent(() => { + this.setCommentEditorDecorations(); + this._commentEditorIsEmpty.set(!this._commentEditor.getValue()); + })); + if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { this.createTextModelListener(); } @@ -426,9 +482,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._formActions = dom.append(this._commentForm, dom.$('.form-actions')); if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { this.createCommentWidgetActions2(this._formActions, model); - if (!fromTemplate) { - this.createCommentWidgetActionsListener(this._formActions, model); - } + this.createCommentWidgetActionsListener(this._formActions, model); } else { this.createCommentWidgetActions(this._formActions, model); } @@ -462,7 +516,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget uri: this._commentEditor.getModel()!.uri, value: this._commentEditor.getValue() }; - this.commentService.setActiveCommentThread(this._commentThread); })); this._commentThreadDisposables.push(this._commentEditor.getModel()!.onDidChangeContent(() => { @@ -674,7 +727,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget uri: this._commentEditor.getModel()!.uri, value: this._commentEditor.getValue() }; - this.commentService.setActiveCommentThread(this._commentThread); await this.commandService.executeCommand(acceptInputCommand.id, ...(acceptInputCommand.arguments || [])); })); @@ -699,11 +751,29 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget uri: this._commentEditor.getModel()!.uri, value: this._commentEditor.getValue() }; - this.commentService.setActiveCommentThread(this._commentThread); await this.commandService.executeCommand(command.id, ...(command.arguments || [])); })); }); } + + const menu = this._commentMenus.getCommentThreadActions(commentThread, this._contextKeyService); + + this._disposables.push(menu); + this._disposables.push(menu.onDidChange(() => { + this._commentFormActions.setActions(menu); + })); + + this._commentFormActions = new CommentFormActions(container, (action: IAction) => { + action.run({ + thread: this._commentThread, + text: this._commentEditor.getValue(), + $mid: 8 + }); + + this.hideReplyArea(); + }, this.themeService); + + this._commentFormActions.setActions(menu); } private createNewCommentNode(comment: modes.Comment): CommentNode { @@ -751,7 +821,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget uri: this._commentEditor.getModel()!.uri, value: this._commentEditor.getValue() }; - this.commentService.setActiveCommentThread(this._commentThread); let commandId = commentThread.acceptInputCommand.id; let args = commentThread.acceptInputCommand.arguments || []; @@ -827,14 +896,13 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } - private createThreadLabel(replaceTemplate: boolean = false) { + private createThreadLabel() { let label: string | undefined; if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { label = (this._commentThread as modes.CommentThread2).label; } - if (label === undefined && !replaceTemplate) { - // if it's for replacing the comment thread template, the comment thread widget title can be undefined as extensions may set it later + if (label === undefined) { if (this._commentThread.comments && this._commentThread.comments.length) { const participantsList = this._commentThread.comments.filter(arrays.uniqueFilter(comment => comment.userName)).map(comment => `@${comment.userName}`).join(', '); label = nls.localize('commentThreadParticipants', "Participants: {0}", participantsList); @@ -847,7 +915,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._headingLabel.innerHTML = strings.escape(label); this._headingLabel.setAttribute('aria-label', label); } - } private expandReplyArea() { @@ -857,6 +924,17 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } + private hideReplyArea() { + this._commentEditor.setValue(''); + this._pendingComment = ''; + if (dom.hasClass(this._commentForm, 'expand')) { + dom.removeClass(this._commentForm, 'expand'); + } + this._commentEditor.getDomNode()!.style.outline = ''; + this._error.textContent = ''; + dom.addClass(this._error, 'hidden'); + } + private createReplyButton() { this._reviewThreadReplyButton = dom.append(this._commentForm, dom.$('button.review-thread-reply-button')); if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index 38140d8bf8..253048b4d3 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -64,7 +64,7 @@ class CommentingRangeDecoration { return this._decorationId; } - constructor(private _editor: ICodeEditor, private _ownerId: string, private _extensionId: string | undefined, private _label: string | undefined, private _range: IRange, private _reply: modes.Command | undefined, commentingOptions: ModelDecorationOptions, private _template: modes.CommentThreadTemplate | undefined, private commentingRangesInfo?: modes.CommentingRanges) { + constructor(private _editor: ICodeEditor, private _ownerId: string, private _extensionId: string | undefined, private _label: string | undefined, private _range: IRange, private _reply: modes.Command | undefined, commentingOptions: ModelDecorationOptions, private commentingRangesInfo?: modes.CommentingRanges) { const startLineNumber = _range.startLineNumber; const endLineNumber = _range.endLineNumber; let commentingRangeDecorations = [{ @@ -81,14 +81,13 @@ class CommentingRangeDecoration { } } - public getCommentAction(): { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined, template: modes.CommentThreadTemplate | undefined } { + public getCommentAction(): { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined } { return { extensionId: this._extensionId, label: this._label, replyCommand: this._reply, ownerId: this._ownerId, - commentingRangesInfo: this.commentingRangesInfo, - template: this._template + commentingRangesInfo: this.commentingRangesInfo }; } @@ -125,11 +124,11 @@ class CommentingRangeDecorator { for (const info of commentInfos) { if (Array.isArray(info.commentingRanges)) { info.commentingRanges.forEach(range => { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, info.reply, this.decorationOptions, info.template)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, info.reply, this.decorationOptions)); }); } else { (info.commentingRanges ? info.commentingRanges.ranges : []).forEach(range => { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, undefined, this.decorationOptions, info.template, info.commentingRanges as modes.CommentingRanges)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, undefined, this.decorationOptions, info.commentingRanges as modes.CommentingRanges)); }); } } @@ -424,7 +423,7 @@ export class ReviewController implements IEditorContribution { } removed.forEach(thread => { - let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); + let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== ''); if (matchedZones.length) { let matchedZone = matchedZones[0]; let index = this._commentWidgets.indexOf(matchedZone); @@ -449,7 +448,7 @@ export class ReviewController implements IEditorContribution { let matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && (zoneWidget.commentThread as any).commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); if (matchedNewCommentThreadZones.length) { - matchedNewCommentThreadZones[0].update(thread, true); + matchedNewCommentThreadZones[0].update(thread); return; } @@ -469,22 +468,6 @@ export class ReviewController implements IEditorContribution { this._commentWidgets.push(zoneWidget); } - private addCommentThreadFromTemplate(lineNumber: number, ownerId: string): ReviewZoneWidget { - let templateCommentThread = this.commentService.getCommentThreadFromTemplate(ownerId, this.editor.getModel()!.uri, { - startLineNumber: lineNumber, - startColumn: 1, - endLineNumber: lineNumber, - endColumn: 1 - })!; - - templateCommentThread.collapsibleState = modes.CommentThreadCollapsibleState.Expanded; - templateCommentThread.comments = []; - - let templateReviewZoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, ownerId, templateCommentThread, '', modes.DraftMode.NotSupported); - - return templateReviewZoneWidget; - } - private addComment(lineNumber: number, replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, draftMode: modes.DraftMode | undefined, pendingComment: string | null) { if (this._newCommentWidget) { this.notificationService.warn(`Please submit the comment at line ${this._newCommentWidget.position ? this._newCommentWidget.position.lineNumber : -1} before creating a new one.`); @@ -640,16 +623,16 @@ export class ReviewController implements IEditorContribution { const commentInfos = newCommentInfos.filter(info => info.ownerId === pick.id); if (commentInfos.length) { - const { replyCommand, ownerId, extensionId, commentingRangesInfo, template } = commentInfos[0]; - this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo, template); + const { replyCommand, ownerId, extensionId, commentingRangesInfo } = commentInfos[0]; + this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo); } }).then(() => { this._addInProgress = false; }); } } else { - const { replyCommand, ownerId, extensionId, commentingRangesInfo, template } = newCommentInfos[0]!; - this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo, template); + const { replyCommand, ownerId, extensionId, commentingRangesInfo } = newCommentInfos[0]!; + this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo); } return Promise.resolve(); @@ -668,11 +651,11 @@ export class ReviewController implements IEditorContribution { return picks; } - private getContextMenuActions(commentInfos: { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined, template: modes.CommentThreadTemplate | undefined }[], lineNumber: number): (IAction | ContextSubMenu)[] { + private getContextMenuActions(commentInfos: { replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined }[], lineNumber: number): (IAction | ContextSubMenu)[] { const actions: (IAction | ContextSubMenu)[] = []; commentInfos.forEach(commentInfo => { - const { replyCommand, ownerId, extensionId, label, commentingRangesInfo, template } = commentInfo; + const { replyCommand, ownerId, extensionId, label, commentingRangesInfo } = commentInfo; actions.push(new Action( 'addCommentThread', @@ -680,7 +663,7 @@ export class ReviewController implements IEditorContribution { undefined, true, () => { - this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo, template); + this.addCommentAtLine2(lineNumber, replyCommand, ownerId, extensionId, commentingRangesInfo); return Promise.resolve(); } )); @@ -688,23 +671,10 @@ export class ReviewController implements IEditorContribution { return actions; } - public addCommentAtLine2(lineNumber: number, replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined, template: modes.CommentThreadTemplate | undefined) { + public addCommentAtLine2(lineNumber: number, replyCommand: modes.Command | undefined, ownerId: string, extensionId: string | undefined, commentingRangesInfo: modes.CommentingRanges | undefined) { if (commentingRangesInfo) { let range = new Range(lineNumber, 1, lineNumber, 1); - if (template) { - // create comment widget through template - let commentThreadWidget = this.addCommentThreadFromTemplate(lineNumber, ownerId); - commentThreadWidget.display(lineNumber, true); - this._commentWidgets.push(commentThreadWidget); - commentThreadWidget.onDidClose(() => { - this._commentWidgets = this._commentWidgets.filter(zoneWidget => !( - zoneWidget.owner === commentThreadWidget.owner && - (zoneWidget.commentThread as any).commentThreadHandle === -1 && - Range.equalsRange(zoneWidget.commentThread.range, commentThreadWidget.commentThread.range) - )); - }); - this.processNextThreadToAdd(); - } else if (commentingRangesInfo.newCommentThreadCallback) { + if (commentingRangesInfo.newCommentThreadCallback) { return commentingRangesInfo.newCommentThreadCallback(this.editor.getModel()!.uri, range) .then(_ => { this.processNextThreadToAdd(); @@ -713,6 +683,11 @@ export class ReviewController implements IEditorContribution { this.notificationService.error(nls.localize('commentThreadAddFailure', "Adding a new comment thread failed: {0}.", e.message)); this.processNextThreadToAdd(); }); + } else { + // latest api, no comments creation callback + this.commentService.createCommentThreadTemplate(ownerId, this.editor.getModel()!.uri, range); + this.processNextThreadToAdd(); + return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check } } else { const commentInfo = this._commentInfos.filter(info => info.owner === ownerId); diff --git a/src/vs/workbench/contrib/comments/browser/commentsPanel.ts b/src/vs/workbench/contrib/comments/browser/commentsPanel.ts index ebed730710..68ac2adfdc 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsPanel.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsPanel.ts @@ -19,11 +19,12 @@ import { ReviewController } from 'vs/workbench/contrib/comments/browser/comments import { CommentsDataFilter, CommentsDataSource, CommentsModelRenderer } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; import { ICommentService, IWorkspaceCommentThreadsEvent } from 'vs/workbench/contrib/comments/browser/commentService'; import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ResourceLabels } from 'vs/workbench/browser/labels'; +import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; export const COMMENTS_PANEL_ID = 'workbench.panel.comments'; export const COMMENTS_PANEL_TITLE = 'Comments'; @@ -266,3 +267,14 @@ export class CommentsPanel extends Panel { } } } + +CommandsRegistry.registerCommand({ + id: 'workbench.action.focusCommentsPanel', + handler: (accessor) => { + const panelService = accessor.get(IPanelService); + const panels = panelService.getPanels(); + if (panels.some(panelIdentifier => panelIdentifier.id === COMMENTS_PANEL_ID)) { + panelService.openPanel(COMMENTS_PANEL_ID, true); + } + } +}); \ No newline at end of file diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index 96b5a80c2a..f6e500ff2e 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -198,6 +198,16 @@ background-image: url(./reaction-hc.svg); } +.monaco-editor .review-widget .body .review-comment .comment-title .action-label { + display: block; + height: 18px; + line-height: 18px; + min-width: 28px; + background-size: 16px; + background-position: center center; + background-repeat: no-repeat; +} + .monaco-editor .review-widget .body .review-comment .comment-title .action-label.toolbar-toggle-pickReactions { background-image: url(./reaction.svg); width: 18px; diff --git a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts index 0944ecf0c1..431c38e425 100644 --- a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts +++ b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts @@ -22,6 +22,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; +import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; export const ctxCommentEditorFocused = new RawContextKey('commentEditorFocused', false); @@ -30,6 +31,7 @@ export class SimpleCommentEditor extends CodeEditorWidget { private _parentEditor: ICodeEditor; private _parentThread: ICommentThreadWidget; private _commentEditorFocused: IContextKey; + private _commentEditorEmpty: IContextKey; constructor( domElement: HTMLElement, @@ -56,11 +58,15 @@ export class SimpleCommentEditor extends CodeEditorWidget { super(domElement, options, codeEditorWidgetOptions, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService); - this._commentEditorFocused = ctxCommentEditorFocused.bindTo(this._contextKeyService); + this._commentEditorFocused = ctxCommentEditorFocused.bindTo(contextKeyService); + this._commentEditorEmpty = CommentContextKeys.commentIsEmpty.bindTo(contextKeyService); + this._commentEditorEmpty.set(!this.getValue()); this._parentEditor = parentEditor; this._parentThread = parentThread; this._register(this.onDidFocusEditorWidget(_ => this._commentEditorFocused.set(true))); + + this._register(this.onDidChangeModelContent(e => this._commentEditorEmpty.set(!this.getValue()))); this._register(this.onDidBlurEditorWidget(_ => this._commentEditorFocused.reset())); } diff --git a/src/vs/workbench/contrib/comments/common/commentContextKeys.ts b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts new file mode 100644 index 0000000000..ef1788c738 --- /dev/null +++ b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export namespace CommentContextKeys { + /** + * A context key that is set when the comment thread has no comments. + */ + export const commentThreadIsEmpty = new RawContextKey('commentThreadIsEmpty', false); + /** + * A context key that is set when the comment has no input. + */ + export const commentIsEmpty = new RawContextKey('commentIsEmpty', false); +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index 632563c33b..c6d1aa593e 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -161,7 +161,7 @@ export abstract class AbstractExpressionsRenderer implements ITreeRenderer { + private async openInternal(input: EditorInput, options: EditorOptions): Promise { if (input instanceof FileEditorInput) { input.setForceOpenAsText(); - return this.editorService.openEditor(input, options, this.group).then(() => undefined); + await this.editorService.openEditor(input, options, this.group); } - - return Promise.resolve(); } - private openExternal(resource: URI): void { - this.windowsService.openExternal(resource.toString()).then(didOpen => { - if (!didOpen) { - return this.windowsService.showItemInFolder(resource); - } - - return undefined; - }); + private async openExternal(resource: URI): Promise { + const didOpen = await this.windowsService.openExternal(resource.toString()); + if (!didOpen) { + return this.windowsService.showItemInFolder(resource); + } } getTitle(): string | null { diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts index 16896f2138..d02b19b0b2 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts @@ -135,7 +135,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut private handleDeletes(arg1: URI | FileChangesEvent, isExternal: boolean, movedTo?: URI): void { const nonDirtyFileEditors = this.getOpenedFileEditors(false /* non-dirty only */); - nonDirtyFileEditors.forEach(editor => { + nonDirtyFileEditors.forEach(async editor => { const resource = editor.getResource(); // Handle deletes in opened editors depending on: @@ -170,20 +170,17 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut // file is really gone and not just a faulty file event. // This only applies to external file events, so we need to check for the isExternal // flag. - let checkExists: Promise; + let exists = false; if (isExternal) { - checkExists = timeout(100).then(() => this.fileService.exists(resource)); - } else { - checkExists = Promise.resolve(false); + await timeout(100); + exists = await this.fileService.exists(resource); } - checkExists.then(exists => { - if (!exists && !editor.isDisposed()) { - editor.dispose(); - } else if (this.environmentService.verbose) { - console.warn(`File exists even though we received a delete event: ${resource.toString()}`); - } - }); + if (!exists && !editor.isDisposed()) { + editor.dispose(); + } else if (this.environmentService.verbose) { + console.warn(`File exists even though we received a delete event: ${resource.toString()}`); + } } }); } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 950e3d6b06..9588cc2dd7 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -125,101 +125,103 @@ export class TextFileEditor extends BaseTextEditor { } } - setInput(input: FileEditorInput, options: EditorOptions, token: CancellationToken): Promise { + async setInput(input: FileEditorInput, options: EditorOptions, token: CancellationToken): Promise { // Update/clear view settings if input changes this.doSaveOrClearTextEditorViewState(this.input); // Set input and resolve - return super.setInput(input, options, token).then(() => { - return input.resolve().then(resolvedModel => { + await super.setInput(input, options, token); + try { + const resolvedModel = await input.resolve(); - // Check for cancellation - if (token.isCancellationRequested) { - return undefined; - } + // Check for cancellation + if (token.isCancellationRequested) { + return; + } - // There is a special case where the text editor has to handle binary file editor input: if a binary file - // has been resolved and cached before, it maybe an actual instance of BinaryEditorModel. In this case our text - // editor has to open this model using the binary editor. We return early in this case. - if (resolvedModel instanceof BinaryEditorModel) { - return this.openAsBinary(input, options); - } + // There is a special case where the text editor has to handle binary file editor input: if a binary file + // has been resolved and cached before, it maybe an actual instance of BinaryEditorModel. In this case our text + // editor has to open this model using the binary editor. We return early in this case. + if (resolvedModel instanceof BinaryEditorModel) { + return this.openAsBinary(input, options); + } - const textFileModel = resolvedModel; + const textFileModel = resolvedModel; - // Editor - const textEditor = this.getControl(); - textEditor.setModel(textFileModel.textEditorModel); + // Editor + const textEditor = this.getControl(); + textEditor.setModel(textFileModel.textEditorModel); - // Always restore View State if any associated - const editorViewState = this.loadTextEditorViewState(this.input.getResource()); - if (editorViewState) { - textEditor.restoreViewState(editorViewState); - } + // Always restore View State if any associated + const editorViewState = this.loadTextEditorViewState(this.input.getResource()); + if (editorViewState) { + textEditor.restoreViewState(editorViewState); + } - // TextOptions (avoiding instanceof here for a reason, do not change!) - if (options && types.isFunction((options).apply)) { - (options).apply(textEditor, ScrollType.Immediate); - } + // TextOptions (avoiding instanceof here for a reason, do not change!) + if (options && types.isFunction((options).apply)) { + (options).apply(textEditor, ScrollType.Immediate); + } - // Readonly flag - textEditor.updateOptions({ readOnly: textFileModel.isReadonly() }); - }, error => { + // Readonly flag + textEditor.updateOptions({ readOnly: textFileModel.isReadonly() }); + } catch (error) { - // In case we tried to open a file inside the text editor and the response - // indicates that this is not a text file, reopen the file through the binary - // editor. - if ((error).textFileOperationResult === TextFileOperationResult.FILE_IS_BINARY) { - return this.openAsBinary(input, options); - } + // In case we tried to open a file inside the text editor and the response + // indicates that this is not a text file, reopen the file through the binary + // editor. + if ((error).textFileOperationResult === TextFileOperationResult.FILE_IS_BINARY) { + return this.openAsBinary(input, options); + } - // Similar, handle case where we were asked to open a folder in the text editor. - if ((error).fileOperationResult === FileOperationResult.FILE_IS_DIRECTORY) { - this.openAsFolder(input); + // Similar, handle case where we were asked to open a folder in the text editor. + if ((error).fileOperationResult === FileOperationResult.FILE_IS_DIRECTORY) { + this.openAsFolder(input); - return Promise.reject(new Error(nls.localize('openFolderError', "File is a directory"))); - } + throw new Error(nls.localize('openFolderError', "File is a directory")); + } - // Offer to create a file from the error if we have a file not found and the name is valid - if ((error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && isValidBasename(basename(input.getResource()))) { - return Promise.reject(createErrorWithActions(toErrorMessage(error), { - actions: [ - new Action('workbench.files.action.createMissingFile', nls.localize('createFile', "Create File"), undefined, true, () => { - return this.textFileService.create(input.getResource()).then(() => this.editorService.openEditor({ - resource: input.getResource(), - options: { - pinned: true // new file gets pinned by default - } - })); - }) - ] - })); - } + // Offer to create a file from the error if we have a file not found and the name is valid + if ((error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && isValidBasename(basename(input.getResource()))) { + throw createErrorWithActions(toErrorMessage(error), { + actions: [ + new Action('workbench.files.action.createMissingFile', nls.localize('createFile', "Create File"), undefined, true, async () => { + await this.textFileService.create(input.getResource()); - if ((error).fileOperationResult === FileOperationResult.FILE_EXCEED_MEMORY_LIMIT) { - const memoryLimit = Math.max(MIN_MAX_MEMORY_SIZE_MB, +this.configurationService.getValue(undefined, 'files.maxMemoryForLargeFilesMB') || FALLBACK_MAX_MEMORY_SIZE_MB); + return this.editorService.openEditor({ + resource: input.getResource(), + options: { + pinned: true // new file gets pinned by default + } + }); + }) + ] + }); + } - return Promise.reject(createErrorWithActions(toErrorMessage(error), { - actions: [ - new Action('workbench.window.action.relaunchWithIncreasedMemoryLimit', nls.localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), undefined, true, () => { - return this.windowsService.relaunch({ - addArgs: [ - `--max-memory=${memoryLimit}` - ] - }); - }), - new Action('workbench.window.action.configureMemoryLimit', nls.localize('configureMemoryLimit', 'Configure Memory Limit'), undefined, true, () => { - return this.preferencesService.openGlobalSettings(undefined, { query: 'files.maxMemoryForLargeFilesMB' }); - }) - ] - })); - } + if ((error).fileOperationResult === FileOperationResult.FILE_EXCEED_MEMORY_LIMIT) { + const memoryLimit = Math.max(MIN_MAX_MEMORY_SIZE_MB, +this.configurationService.getValue(undefined, 'files.maxMemoryForLargeFilesMB') || FALLBACK_MAX_MEMORY_SIZE_MB); - // Otherwise make sure the error bubbles up - return Promise.reject(error); - }); - }); + throw createErrorWithActions(toErrorMessage(error), { + actions: [ + new Action('workbench.window.action.relaunchWithIncreasedMemoryLimit', nls.localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), undefined, true, () => { + return this.windowsService.relaunch({ + addArgs: [ + `--max-memory=${memoryLimit}` + ] + }); + }), + new Action('workbench.window.action.configureMemoryLimit', nls.localize('configureMemoryLimit', 'Configure Memory Limit'), undefined, true, () => { + return this.preferencesService.openGlobalSettings(undefined, { query: 'files.maxMemoryForLargeFilesMB' }); + }) + ] + }); + } + + // Otherwise make sure the error bubbles up + throw error; + } } private openAsBinary(input: FileEditorInput, options: EditorOptions): void { @@ -227,21 +229,20 @@ export class TextFileEditor extends BaseTextEditor { this.editorService.openEditor(input, options, this.group); } - private openAsFolder(input: FileEditorInput): void { + private async openAsFolder(input: FileEditorInput): Promise { if (!this.group) { return; } // Since we cannot open a folder, we have to restore the previous input if any and close the editor - this.group.closeEditor(this.input).then(() => { + await this.group.closeEditor(this.input); - // Best we can do is to reveal the folder in the explorer - if (this.contextService.isInsideWorkspace(input.getResource())) { - this.viewletService.openViewlet(VIEWLET_ID).then(() => { - this.explorerService.select(input.getResource(), true); - }); - } - }); + // Best we can do is to reveal the folder in the explorer + if (this.contextService.isInsideWorkspace(input.getResource())) { + await this.viewletService.openViewlet(VIEWLET_ID); + + this.explorerService.select(input.getResource(), true); + } } protected getAriaLabel(): string { diff --git a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts index b58543f9c8..f351dc5726 100644 --- a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts @@ -240,26 +240,26 @@ class ResolveSaveConflictAction extends Action { super('workbench.files.action.resolveConflict', nls.localize('compareChanges', "Compare")); } - run(): Promise { + async run(): Promise { if (!this.model.isDisposed()) { const resource = this.model.getResource(); const name = basename(resource); const editorLabel = nls.localize('saveConflictDiffLabel', "{0} (in file) ↔ {1} (in {2}) - Resolve save conflict", name, name, this.environmentService.appNameLong); - return TextFileContentProvider.open(resource, CONFLICT_RESOLUTION_SCHEME, editorLabel, this.editorService, { pinned: true }).then(() => { - if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.GLOBAL)) { - return; // return if this message is ignored - } + await TextFileContentProvider.open(resource, CONFLICT_RESOLUTION_SCHEME, editorLabel, this.editorService, { pinned: true }); - // Show additional help how to resolve the save conflict - const actions: INotificationActions = { primary: [], secondary: [] }; - actions.primary!.push(this.instantiationService.createInstance(ResolveConflictLearnMoreAction)); - actions.secondary!.push(this.instantiationService.createInstance(DoNotShowResolveConflictLearnMoreAction)); + if (this.storageService.getBoolean(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, StorageScope.GLOBAL)) { + return; // return if this message is ignored + } - const handle = this.notificationService.notify({ severity: Severity.Info, message: conflictEditorHelp, actions }); - Event.once(handle.onDidClose)(() => dispose(...actions.primary!, ...actions.secondary!)); - pendingResolveSaveConflictMessages.push(handle); - }); + // Show additional help how to resolve the save conflict + const actions: INotificationActions = { primary: [], secondary: [] }; + actions.primary!.push(this.instantiationService.createInstance(ResolveConflictLearnMoreAction)); + actions.secondary!.push(this.instantiationService.createInstance(DoNotShowResolveConflictLearnMoreAction)); + + const handle = this.notificationService.notify({ severity: Severity.Info, message: conflictEditorHelp, actions }); + Event.once(handle.onDidClose)(() => dispose(...actions.primary!, ...actions.secondary!)); + pendingResolveSaveConflictMessages.push(handle); } return Promise.resolve(true); @@ -316,31 +316,28 @@ export const acceptLocalChangesCommand = (accessor: ServicesAccessor, resource: const editor = control.input; const group = control.group; - resolverService.createModelReference(resource).then(reference => { + resolverService.createModelReference(resource).then(async reference => { const model = reference.object as IResolvedTextFileEditorModel; const localModelSnapshot = model.createSnapshot(); clearPendingResolveSaveConflictMessages(); // hide any previously shown message about how to use these actions // Revert to be able to save - return model.revert().then(() => { + await model.revert(); - // Restore user value (without loosing undo stack) - modelService.updateModel(model.textEditorModel, createTextBufferFactoryFromSnapshot(localModelSnapshot)); + // Restore user value (without loosing undo stack) + modelService.updateModel(model.textEditorModel, createTextBufferFactoryFromSnapshot(localModelSnapshot)); - // Trigger save - return model.save().then(() => { + // Trigger save + await model.save(); - // Reopen file input - return editorService.openEditor({ resource: model.getResource() }, group).then(() => { + // Reopen file input + await editorService.openEditor({ resource: model.getResource() }, group); - // Clean up - group.closeEditor(editor); - editor.dispose(); - reference.dispose(); - }); - }); - }); + // Clean up + group.closeEditor(editor); + editor.dispose(); + reference.dispose(); }); }; @@ -355,22 +352,20 @@ export const revertLocalChangesCommand = (accessor: ServicesAccessor, resource: const editor = control.input; const group = control.group; - resolverService.createModelReference(resource).then(reference => { + resolverService.createModelReference(resource).then(async reference => { const model = reference.object as ITextFileEditorModel; clearPendingResolveSaveConflictMessages(); // hide any previously shown message about how to use these actions // Revert on model - return model.revert().then(() => { + await model.revert(); - // Reopen file input - return editorService.openEditor({ resource: model.getResource() }, group).then(() => { + // Reopen file input + await editorService.openEditor({ resource: model.getResource() }, group); - // Clean up - group.closeEditor(editor); - editor.dispose(); - reference.dispose(); - }); - }); + // Clean up + group.closeEditor(editor); + editor.dispose(); + reference.dispose(); }); }; diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index 89b9b9c861..c1c82c58f6 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts @@ -273,16 +273,17 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { return this.doResolveAsText(); } - private doResolveAsText(): Promise { + private async doResolveAsText(): Promise { // Resolve as text - return this.textFileService.models.loadOrCreate(this.resource, { - mode: this.preferredMode, - encoding: this.preferredEncoding, - reload: { async: true }, // trigger a reload of the model if it exists already but do not wait to show the model - allowBinary: this.forceOpenAsText, - reason: LoadReason.EDITOR - }).then(model => { + try { + await this.textFileService.models.loadOrCreate(this.resource, { + mode: this.preferredMode, + encoding: this.preferredEncoding, + reload: { async: true }, // trigger a reload of the model if it exists already but do not wait to show the model + allowBinary: this.forceOpenAsText, + reason: LoadReason.EDITOR + }); // This is a bit ugly, because we first resolve the model and then resolve a model reference. the reason being that binary // or very large files do not resolve to a text file model but should be opened as binary files without text. First calling into @@ -292,8 +293,10 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { this.textModelReference = this.textModelResolverService.createModelReference(this.resource); } - return this.textModelReference.then(ref => ref.object as TextFileEditorModel); - }, error => { + const ref = await this.textModelReference; + + return ref.object as TextFileEditorModel; + } catch (error) { // In case of an error that indicates that the file is binary or too large, just return with the binary editor model if ( @@ -304,12 +307,12 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { } // Bubble any other error up - return Promise.reject(error); - }); + throw error; + } } - private doResolveAsBinary(): Promise { - return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load().then(m => m as BinaryEditorModel); + private async doResolveAsBinary(): Promise { + return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load(); } isResolved(): boolean { diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index 063b5d0228..2c8dfc0902 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -143,15 +143,13 @@ export class TextFileContentProvider implements ITextModelContentProvider { @IModelService private readonly modelService: IModelService ) { } - static open(resource: URI, scheme: string, label: string, editorService: IEditorService, options?: ITextEditorOptions): Promise { - return editorService.openEditor( - { - leftResource: TextFileContentProvider.resourceToTextFile(scheme, resource), - rightResource: resource, - label, - options - } - ).then(); + static async open(resource: URI, scheme: string, label: string, editorService: IEditorService, options?: ITextEditorOptions): Promise { + await editorService.openEditor({ + leftResource: TextFileContentProvider.resourceToTextFile(scheme, resource), + rightResource: resource, + label, + options + }); } private static resourceToTextFile(scheme: string, resource: URI): URI { @@ -162,56 +160,55 @@ export class TextFileContentProvider implements ITextModelContentProvider { return resource.with({ scheme: JSON.parse(resource.query)['scheme'], query: null }); } - provideTextContent(resource: URI): Promise { + async provideTextContent(resource: URI): Promise { const savedFileResource = TextFileContentProvider.textFileToResource(resource); // Make sure our text file is resolved up to date - return this.resolveEditorModel(resource).then(codeEditorModel => { + const codeEditorModel = await this.resolveEditorModel(resource); - // Make sure to keep contents up to date when it changes - if (!this.fileWatcherDisposable) { - this.fileWatcherDisposable = this.fileService.onFileChanges(changes => { - if (changes.contains(savedFileResource, FileChangeType.UPDATED)) { - this.resolveEditorModel(resource, false /* do not create if missing */); // update model when resource changes - } - }); - - if (codeEditorModel) { - once(codeEditorModel.onWillDispose)(() => { - dispose(this.fileWatcherDisposable); - this.fileWatcherDisposable = undefined; - }); + // Make sure to keep contents up to date when it changes + if (!this.fileWatcherDisposable) { + this.fileWatcherDisposable = this.fileService.onFileChanges(changes => { + if (changes.contains(savedFileResource, FileChangeType.UPDATED)) { + this.resolveEditorModel(resource, false /* do not create if missing */); // update model when resource changes } - } + }); - return codeEditorModel; - }); + if (codeEditorModel) { + once(codeEditorModel.onWillDispose)(() => { + dispose(this.fileWatcherDisposable); + this.fileWatcherDisposable = undefined; + }); + } + } + + return codeEditorModel; } private resolveEditorModel(resource: URI, createAsNeeded?: true): Promise; private resolveEditorModel(resource: URI, createAsNeeded?: boolean): Promise; - private resolveEditorModel(resource: URI, createAsNeeded: boolean = true): Promise { + private async resolveEditorModel(resource: URI, createAsNeeded: boolean = true): Promise { const savedFileResource = TextFileContentProvider.textFileToResource(resource); - return this.textFileService.readStream(savedFileResource).then(content => { - let codeEditorModel = this.modelService.getModel(resource); - if (codeEditorModel) { - this.modelService.updateModel(codeEditorModel, content.value); - } else if (createAsNeeded) { - const textFileModel = this.modelService.getModel(savedFileResource); + const content = await this.textFileService.readStream(savedFileResource); - let languageSelector: ILanguageSelection; - if (textFileModel) { - languageSelector = this.modeService.create(textFileModel.getModeId()); - } else { - languageSelector = this.modeService.createByFilepathOrFirstLine(savedFileResource.path); - } + let codeEditorModel = this.modelService.getModel(resource); + if (codeEditorModel) { + this.modelService.updateModel(codeEditorModel, content.value); + } else if (createAsNeeded) { + const textFileModel = this.modelService.getModel(savedFileResource); - codeEditorModel = this.modelService.createModel(content.value, languageSelector, resource); + let languageSelector: ILanguageSelection; + if (textFileModel) { + languageSelector = this.modeService.create(textFileModel.getModeId()); + } else { + languageSelector = this.modeService.createByFilepathOrFirstLine(savedFileResource.path); } - return codeEditorModel; - }); + codeEditorModel = this.modelService.createModel(content.value, languageSelector, resource); + } + + return codeEditorModel; } dispose(): void { diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts index eca004ce16..d3ea4e5255 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts @@ -34,26 +34,25 @@ suite('Files - FileEditorTracker', () => { accessor = instantiationService.createInstance(ServiceAccessor); }); - test('file change event updates model', function () { + test('file change event updates model', async function () { const tracker = instantiationService.createInstance(FileEditorTracker); const resource = toResource.call(this, '/path/index.txt'); - return accessor.textFileService.models.loadOrCreate(resource).then((model: IResolvedTextFileEditorModel) => { - model.textEditorModel.setValue('Super Good'); - assert.equal(snapshotToString(model.createSnapshot()!), 'Super Good'); + const model = await accessor.textFileService.models.loadOrCreate(resource) as IResolvedTextFileEditorModel; - return model.save().then(() => { + model.textEditorModel.setValue('Super Good'); + assert.equal(snapshotToString(model.createSnapshot()!), 'Super Good'); - // change event (watcher) - accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }])); + await model.save(); - return timeout(0).then(() => { // due to event updating model async - assert.equal(snapshotToString(model.createSnapshot()!), 'Hello Html'); + // change event (watcher) + accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }])); - tracker.dispose(); - }); - }); - }); + await timeout(0); // due to event updating model async + + assert.equal(snapshotToString(model.createSnapshot()!), 'Hello Html'); + + tracker.dispose(); }); }); diff --git a/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts b/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts index 32b3d0a388..f8be9ca478 100644 --- a/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts +++ b/src/vs/workbench/contrib/quickopen/browser/gotoSymbolHandler.ts @@ -390,7 +390,7 @@ export class GotoSymbolHandler extends QuickOpenHandler { this.rangeHighlightDecorationId = undefined; } - getResults(searchValue: string, token: CancellationToken): Promise { + async getResults(searchValue: string, token: CancellationToken): Promise { searchValue = searchValue.trim(); // Support to cancel pending outline requests @@ -407,20 +407,19 @@ export class GotoSymbolHandler extends QuickOpenHandler { } // Resolve Outline Model - return this.getOutline().then(outline => { - if (!outline) { - return outline; - } - - if (token.isCancellationRequested) { - return outline; - } - - // Filter by search - outline.applyFilter(searchValue); - + const outline = await this.getOutline(); + if (!outline) { return outline; - }); + } + + if (token.isCancellationRequested) { + return outline; + } + + // Filter by search + outline.applyFilter(searchValue); + + return outline; } getEmptyLabel(searchString: string): string { diff --git a/src/vs/workbench/contrib/search/browser/openFileHandler.ts b/src/vs/workbench/contrib/search/browser/openFileHandler.ts index 23938578e6..24a03d6887 100644 --- a/src/vs/workbench/contrib/search/browser/openFileHandler.ts +++ b/src/vs/workbench/contrib/search/browser/openFileHandler.ts @@ -147,43 +147,45 @@ export class OpenFileHandler extends QuickOpenHandler { return this.doFindResults(query, token, this.cacheState.cacheKey, maxSortedResults); } - private doFindResults(query: IPreparedQuery, token: CancellationToken, cacheKey?: string, maxSortedResults?: number): Promise { + private async doFindResults(query: IPreparedQuery, token: CancellationToken, cacheKey?: string, maxSortedResults?: number): Promise { const queryOptions = this.doResolveQueryOptions(query, cacheKey, maxSortedResults); - let iconClass: string; + let iconClass: string | undefined = undefined; if (this.options && this.options.forceUseIcons && !this.themeService.getFileIconTheme()) { iconClass = 'file'; // only use a generic file icon if we are forced to use an icon and have no icon theme set otherwise } - return this.getAbsolutePathResult(query).then(result => { - if (token.isCancellationRequested) { - return Promise.resolve({ results: [] }); + let complete: ISearchComplete | undefined = undefined; + + const result = await this.getAbsolutePathResult(query); + if (token.isCancellationRequested) { + complete = { results: [] }; + } + + // If the original search value is an existing file on disk, return it immediately and bypass the search service + else if (result) { + complete = { results: [{ resource: result }] }; + } + + else { + complete = await this.searchService.fileSearch(this.queryBuilder.file(this.contextService.getWorkspace().folders.map(folder => folder.uri), queryOptions), token); + } + + const results: QuickOpenEntry[] = []; + + if (!token.isCancellationRequested) { + for (const fileMatch of complete.results) { + const label = basename(fileMatch.resource); + const description = this.labelService.getUriLabel(dirname(fileMatch.resource), { relative: true }); + + results.push(this.instantiationService.createInstance(FileEntry, fileMatch.resource, label, description, iconClass)); } + } - // If the original search value is an existing file on disk, return it immediately and bypass the search service - if (result) { - return Promise.resolve({ results: [{ resource: result }] }); - } - - return this.searchService.fileSearch(this.queryBuilder.file(this.contextService.getWorkspace().folders.map(folder => folder.uri), queryOptions), token); - }).then(complete => { - const results: QuickOpenEntry[] = []; - - if (!token.isCancellationRequested) { - for (const fileMatch of complete.results) { - - const label = basename(fileMatch.resource); - const description = this.labelService.getUriLabel(dirname(fileMatch.resource), { relative: true }); - - results.push(this.instantiationService.createInstance(FileEntry, fileMatch.resource, label, description, iconClass)); - } - } - - return new FileQuickOpenModel(results, complete.stats); - }); + return new FileQuickOpenModel(results, complete.stats); } - private getAbsolutePathResult(query: IPreparedQuery): Promise { + private async getAbsolutePathResult(query: IPreparedQuery): Promise { const detildifiedQuery = untildify(query.original, this.environmentService.userHome); if (isAbsolute(detildifiedQuery)) { const workspaceFolders = this.contextService.getWorkspace().folders; @@ -191,12 +193,16 @@ export class OpenFileHandler extends QuickOpenHandler { workspaceFolders[0].uri.with({ path: detildifiedQuery }) : URI.file(detildifiedQuery); - return this.fileService.resolve(resource).then( - stat => stat.isDirectory ? undefined : resource, - error => undefined); + try { + const stat = await this.fileService.resolve(resource); + + return stat.isDirectory ? undefined : resource; + } catch (error) { + // ignore + } } - return Promise.resolve(undefined); + return undefined; } private doResolveQueryOptions(query: IPreparedQuery, cacheKey?: string, maxSortedResults?: number): IFileQueryBuilderOptions { diff --git a/src/vs/workbench/contrib/stats/node/workspaceStats.ts b/src/vs/workbench/contrib/stats/node/workspaceStats.ts index 9447f01882..6aa96cb9c9 100644 --- a/src/vs/workbench/contrib/stats/node/workspaceStats.ts +++ b/src/vs/workbench/contrib/stats/node/workspaceStats.ts @@ -53,6 +53,7 @@ const ModulesToLookFor = [ // JS frameworks 'react', 'react-native', + 'rnpm-plugin-windows', '@angular/core', '@ionic', 'vue', @@ -268,6 +269,7 @@ export class WorkspaceStats implements IWorkbenchContribution { "workspace.npm.hapi" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.socket.io" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.restify" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.rnpm-plugin-windows" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.react" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.@angular/core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.vue" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -288,6 +290,7 @@ export class WorkspaceStats implements IWorkbenchContribution { "workspace.reactNative" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.ionic" : { "classification" : "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": "true" }, "workspace.nativeScript" : { "classification" : "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": "true" }, + "workspace.java.pom" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.requirements" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.requirements.star" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.Pipfile" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -387,6 +390,8 @@ export class WorkspaceStats implements IWorkbenchContribution { tags['workspace.npm'] = nameSet.has('package.json') || nameSet.has('node_modules'); tags['workspace.bower'] = nameSet.has('bower.json') || nameSet.has('bower_components'); + tags['workspace.java.pom'] = nameSet.has('pom.xml'); + tags['workspace.yeoman.code.ext'] = nameSet.has('vsc-extension-quickstart.md'); tags['workspace.py.requirements'] = nameSet.has('requirements.txt'); @@ -502,23 +507,24 @@ export class WorkspaceStats implements IWorkbenchContribution { const packageJsonPromises = getFilePromises('package.json', this.fileService, this.textFileService, content => { try { const packageJsonContents = JSON.parse(content.value); - if (packageJsonContents['dependencies']) { - for (let module of ModulesToLookFor) { - if ('react-native' === module) { - if (packageJsonContents['dependencies'][module]) { - tags['workspace.reactNative'] = true; - } - } else if ('tns-core-modules' === module) { - if (packageJsonContents['dependencies'][module]) { - tags['workspace.nativescript'] = true; - } - } else { - if (packageJsonContents['dependencies'][module]) { - tags['workspace.npm.' + module] = true; - } + let dependencies = packageJsonContents['dependencies']; + let devDependencies = packageJsonContents['devDependencies']; + for (let module of ModulesToLookFor) { + if ('react-native' === module) { + if ((dependencies && dependencies[module]) || (devDependencies && devDependencies[module])) { + tags['workspace.reactNative'] = true; + } + } else if ('tns-core-modules' === module) { + if ((dependencies && dependencies[module]) || (devDependencies && devDependencies[module])) { + tags['workspace.nativescript'] = true; + } + } else { + if ((dependencies && dependencies[module]) || (devDependencies && devDependencies[module])) { + tags['workspace.npm.' + module] = true; } } } + } catch (e) { // Ignore errors when resolving file or parsing file contents diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 17cab36859..2b9d8448ad 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -165,8 +165,7 @@ class WebviewPortMappingProvider extends Disposable { session.onBeforeRequest(async (details) => { const uri = URI.parse(details.url); - const allowedSchemes = ['http', 'https', 'ws', 'wss']; - if (allowedSchemes.indexOf(uri.scheme) === -1) { + if (uri.scheme !== 'http' && uri.scheme !== 'https') { return undefined; } diff --git a/src/vs/workbench/electron-browser/actions/helpActions.ts b/src/vs/workbench/electron-browser/actions/helpActions.ts index d75b261248..9185f28c8c 100644 --- a/src/vs/workbench/electron-browser/actions/helpActions.ts +++ b/src/vs/workbench/electron-browser/actions/helpActions.ts @@ -113,11 +113,10 @@ export class OpenNewsletterSignupUrlAction extends Action { this.telemetryService = telemetryService; } - run(): Promise { - this.telemetryService.getTelemetryInfo().then(info => { - window.open(`${OpenNewsletterSignupUrlAction.URL}?machineId=${encodeURIComponent(info.machineId)}`); - }); - return Promise.resolve(); + async run(): Promise { + const info = await this.telemetryService.getTelemetryInfo(); + + window.open(`${OpenNewsletterSignupUrlAction.URL}?machineId=${encodeURIComponent(info.machineId)}`); } } diff --git a/src/vs/workbench/electron-browser/actions/windowActions.ts b/src/vs/workbench/electron-browser/actions/windowActions.ts index 65ecbac249..86166c9768 100644 --- a/src/vs/workbench/electron-browser/actions/windowActions.ts +++ b/src/vs/workbench/electron-browser/actions/windowActions.ts @@ -86,7 +86,7 @@ export abstract class BaseZoomAction extends Action { super(id, label); } - protected setConfiguredZoomLevel(level: number): void { + protected async setConfiguredZoomLevel(level: number): Promise { level = Math.round(level); // when reaching smallest zoom, prevent fractional zoom levels const applyZoom = () => { @@ -98,7 +98,9 @@ export abstract class BaseZoomAction extends Action { browser.setZoomLevel(webFrame.getZoomLevel(), /*isTrusted*/false); }; - this.configurationService.updateValue(BaseZoomAction.SETTING_KEY, level).then(() => applyZoom()); + await this.configurationService.updateValue(BaseZoomAction.SETTING_KEY, level); + + applyZoom(); } } @@ -175,8 +177,10 @@ export class ReloadWindowAction extends Action { super(id, label); } - run(): Promise { - return this.windowService.reloadWindow().then(() => true); + async run(): Promise { + await this.windowService.reloadWindow(); + + return true; } } @@ -193,8 +197,10 @@ export class ReloadWindowWithExtensionsDisabledAction extends Action { super(id, label); } - run(): Promise { - return this.windowService.reloadWindow({ _: [], 'disable-extensions': true }).then(() => true); + async run(): Promise { + await this.windowService.reloadWindow({ _: [], 'disable-extensions': true }); + + return true; } } @@ -221,41 +227,38 @@ export abstract class BaseSwitchWindow extends Action { protected abstract isQuickNavigate(): boolean; - run(): Promise { + async run(): Promise { const currentWindowId = this.windowService.windowId; - return this.windowsService.getWindows().then(windows => { - const placeHolder = nls.localize('switchWindowPlaceHolder', "Select a window to switch to"); - const picks = windows.map(win => { - const resource = win.filename ? URI.file(win.filename) : win.folderUri ? win.folderUri : win.workspace ? win.workspace.configPath : undefined; - const fileKind = win.filename ? FileKind.FILE : win.workspace ? FileKind.ROOT_FOLDER : win.folderUri ? FileKind.FOLDER : FileKind.FILE; - return { - payload: win.id, - label: win.title, - iconClasses: getIconClasses(this.modelService, this.modeService, resource, fileKind), - description: (currentWindowId === win.id) ? nls.localize('current', "Current Window") : undefined, - buttons: (!this.isQuickNavigate() && currentWindowId !== win.id) ? [this.closeWindowAction] : undefined - }; - }); + const windows = await this.windowsService.getWindows(); + const placeHolder = nls.localize('switchWindowPlaceHolder', "Select a window to switch to"); + const picks = windows.map(win => { + const resource = win.filename ? URI.file(win.filename) : win.folderUri ? win.folderUri : win.workspace ? win.workspace.configPath : undefined; + const fileKind = win.filename ? FileKind.FILE : win.workspace ? FileKind.ROOT_FOLDER : win.folderUri ? FileKind.FOLDER : FileKind.FILE; + return { + payload: win.id, + label: win.title, + iconClasses: getIconClasses(this.modelService, this.modeService, resource, fileKind), + description: (currentWindowId === win.id) ? nls.localize('current', "Current Window") : undefined, + buttons: (!this.isQuickNavigate() && currentWindowId !== win.id) ? [this.closeWindowAction] : undefined + }; + }); + const autoFocusIndex = (picks.indexOf(picks.filter(pick => pick.payload === currentWindowId)[0]) + 1) % picks.length; - const autoFocusIndex = (picks.indexOf(picks.filter(pick => pick.payload === currentWindowId)[0]) + 1) % picks.length; - - return this.quickInputService.pick(picks, { - contextKey: 'inWindowsPicker', - activeItem: picks[autoFocusIndex], - placeHolder, - quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined, - onDidTriggerItemButton: context => { - this.windowsService.closeWindow(context.item.payload).then(() => { - context.removeItem(); - }); - } - }); - }).then(pick => { - if (pick) { - this.windowsService.focusWindow(pick.payload); + const pick = await this.quickInputService.pick(picks, { + contextKey: 'inWindowsPicker', + activeItem: picks[autoFocusIndex], + placeHolder, + quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined, + onDidTriggerItemButton: async context => { + await this.windowsService.closeWindow(context.item.payload); + context.removeItem(); } }); + + if (pick) { + this.windowsService.focusWindow(pick.payload); + } } } @@ -331,12 +334,13 @@ export abstract class BaseOpenRecentAction extends Action { protected abstract isQuickNavigate(): boolean; - run(): Promise { - return this.windowService.getRecentlyOpened() - .then(({ workspaces, files }) => this.openRecent(workspaces, files)); + async run(): Promise { + const { workspaces, files } = await this.windowService.getRecentlyOpened(); + + this.openRecent(workspaces, files); } - private openRecent(recentWorkspaces: Array, recentFiles: IRecentFile[]): void { + private async openRecent(recentWorkspaces: Array, recentFiles: IRecentFile[]): Promise { const toPick = (recent: IRecent, labelService: ILabelService, buttons: IQuickInputButton[] | undefined) => { let uriToOpen: IURIToOpen | undefined; @@ -376,26 +380,26 @@ export abstract class BaseOpenRecentAction extends Action { const firstEntry = recentWorkspaces[0]; let autoFocusSecondEntry: boolean = firstEntry && this.contextService.isCurrentWorkspace(isRecentWorkspace(firstEntry) ? firstEntry.workspace : firstEntry.folderUri); - let keyMods: IKeyMods; + let keyMods: IKeyMods | undefined; const workspaceSeparator: IQuickPickSeparator = { type: 'separator', label: nls.localize('workspaces', "workspaces") }; const fileSeparator: IQuickPickSeparator = { type: 'separator', label: nls.localize('files', "files") }; const picks = [workspaceSeparator, ...workspacePicks, fileSeparator, ...filePicks]; - this.quickInputService.pick(picks, { + const pick = await this.quickInputService.pick(picks, { contextKey: inRecentFilesPickerContextKey, activeItem: [...workspacePicks, ...filePicks][autoFocusSecondEntry ? 1 : 0], placeHolder: isMacintosh ? nls.localize('openRecentPlaceHolderMac', "Select to open (hold Cmd-key to open in new window)") : nls.localize('openRecentPlaceHolder', "Select to open (hold Ctrl-key to open in new window)"), matchOnDescription: true, onKeyMods: mods => keyMods = mods, quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined, - onDidTriggerItemButton: context => { - this.windowsService.removeFromRecentlyOpened([context.item.resource]).then(() => context.removeItem()); - } - }).then((pick): Promise | void => { - if (pick) { - const forceNewWindow = keyMods.ctrlCmd; - return this.windowService.openWindow([pick.uriToOpen], { forceNewWindow }); + onDidTriggerItemButton: async context => { + await this.windowsService.removeFromRecentlyOpened([context.item.resource]); + context.removeItem(); } }); + + if (pick) { + return this.windowService.openWindow([pick.uriToOpen], { forceNewWindow: keyMods && keyMods.ctrlCmd }); + } } } diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index 6e6ef14a19..a2d7daece9 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -106,42 +106,39 @@ class CodeRendererMain extends Disposable { } } - open(): Promise { - return this.initServices().then(services => { + async open(): Promise { + const services = await this.initServices(); + await domContentLoaded(); + mark('willStartWorkbench'); - return domContentLoaded().then(() => { - mark('willStartWorkbench'); + // Create Workbench + this.workbench = new Workbench(document.body, services.serviceCollection, services.logService); - // Create Workbench - this.workbench = new Workbench(document.body, services.serviceCollection, services.logService); + // Layout + this._register(addDisposableListener(window, EventType.RESIZE, e => this.onWindowResize(e, true))); - // Layout - this._register(addDisposableListener(window, EventType.RESIZE, e => this.onWindowResize(e, true))); + // Workbench Lifecycle + this._register(this.workbench.onShutdown(() => this.dispose())); + this._register(this.workbench.onWillShutdown(event => event.join(services.storageService.close()))); - // Workbench Lifecycle - this._register(this.workbench.onShutdown(() => this.dispose())); - this._register(this.workbench.onWillShutdown(event => event.join(services.storageService.close()))); + // Startup + const instantiationService = this.workbench.startup(); - // Startup - const instantiationService = this.workbench.startup(); + // Window + this._register(instantiationService.createInstance(ElectronWindow)); - // Window - this._register(instantiationService.createInstance(ElectronWindow)); + // Driver + if (this.configuration.driver) { + instantiationService.invokeFunction(async accessor => this._register(await registerWindowDriver(accessor))); + } - // Driver - if (this.configuration.driver) { - instantiationService.invokeFunction(accessor => registerWindowDriver(accessor).then(disposable => this._register(disposable))); - } + // Config Exporter + if (this.configuration['export-default-configuration']) { + instantiationService.createInstance(DefaultConfigurationExportHelper); + } - // Config Exporter - if (this.configuration['export-default-configuration']) { - instantiationService.createInstance(DefaultConfigurationExportHelper); - } - - // Logging - services.logService.trace('workbench configuration', JSON.stringify(this.configuration)); - }); - }); + // Logging + services.logService.trace('workbench configuration', JSON.stringify(this.configuration)); } private onWindowResize(e: Event, retry: boolean): void { @@ -162,7 +159,7 @@ class CodeRendererMain extends Disposable { } } - private initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: StorageService }> { + private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: StorageService }> { const serviceCollection = new ServiceCollection(); // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -203,7 +200,9 @@ class CodeRendererMain extends Disposable { fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); } - return this.resolveWorkspaceInitializationPayload(environmentService).then(payload => Promise.all([ + const payload = await this.resolveWorkspaceInitializationPayload(environmentService); + + const services = await Promise.all([ this.createWorkspaceService(payload, environmentService, fileService, remoteAgentService, logService).then(service => { // Workspace @@ -222,10 +221,12 @@ class CodeRendererMain extends Disposable { return service; }) - ]).then(services => ({ serviceCollection, logService, storageService: services[1] }))); + ]); + + return { serviceCollection, logService, storageService: services[1] }; } - private resolveWorkspaceInitializationPayload(environmentService: IWorkbenchEnvironmentService): Promise { + private async resolveWorkspaceInitializationPayload(environmentService: IWorkbenchEnvironmentService): Promise { // Multi-root workspace if (this.configuration.workspace) { @@ -233,32 +234,29 @@ class CodeRendererMain extends Disposable { } // Single-folder workspace - let workspaceInitializationPayload: Promise = Promise.resolve(undefined); + let workspaceInitializationPayload: IWorkspaceInitializationPayload | undefined; if (this.configuration.folderUri) { - workspaceInitializationPayload = this.resolveSingleFolderWorkspaceInitializationPayload(this.configuration.folderUri); + workspaceInitializationPayload = await this.resolveSingleFolderWorkspaceInitializationPayload(this.configuration.folderUri); } - return workspaceInitializationPayload.then(payload => { - - // Fallback to empty workspace if we have no payload yet. - if (!payload) { - let id: string; - if (this.configuration.backupPath) { - id = basename(this.configuration.backupPath); // we know the backupPath must be a unique path so we leverage its name as workspace ID - } else if (environmentService.isExtensionDevelopment) { - id = 'ext-dev'; // extension development window never stores backups and is a singleton - } else { - return Promise.reject(new Error('Unexpected window configuration without backupPath')); - } - - payload = { id }; + // Fallback to empty workspace if we have no payload yet. + if (!workspaceInitializationPayload) { + let id: string; + if (this.configuration.backupPath) { + id = basename(this.configuration.backupPath); // we know the backupPath must be a unique path so we leverage its name as workspace ID + } else if (environmentService.isExtensionDevelopment) { + id = 'ext-dev'; // extension development window never stores backups and is a singleton + } else { + throw new Error('Unexpected window configuration without backupPath'); } - return payload; - }); + workspaceInitializationPayload = { id }; + } + + return workspaceInitializationPayload; } - private resolveSingleFolderWorkspaceInitializationPayload(folderUri: ISingleFolderWorkspaceIdentifier): Promise { + private async resolveSingleFolderWorkspaceInitializationPayload(folderUri: ISingleFolderWorkspaceIdentifier): Promise { // Return early the folder is not local if (folderUri.scheme !== Schemas.file) { @@ -285,40 +283,54 @@ class CodeRendererMain extends Disposable { } // For local: ensure path is absolute and exists - const sanitizedFolderPath = sanitizeFilePath(folderUri.fsPath, process.env['VSCODE_CWD'] || process.cwd()); - return stat(sanitizedFolderPath).then(stat => { + try { + const sanitizedFolderPath = sanitizeFilePath(folderUri.fsPath, process.env['VSCODE_CWD'] || process.cwd()); + const fileStat = await stat(sanitizedFolderPath); + const sanitizedFolderUri = URI.file(sanitizedFolderPath); return { - id: computeLocalDiskFolderId(sanitizedFolderUri, stat), + id: computeLocalDiskFolderId(sanitizedFolderUri, fileStat), folder: sanitizedFolderUri }; - }, error => onUnexpectedError(error)); + } catch (error) { + onUnexpectedError(error); + } + + return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check } - private createWorkspaceService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: FileService, remoteAgentService: IRemoteAgentService, logService: ILogService): Promise { + private async createWorkspaceService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: FileService, remoteAgentService: IRemoteAgentService, logService: ILogService): Promise { const configurationFileService = new ConfigurationFileService(); configurationFileService.fileService = fileService; const workspaceService = new WorkspaceService({ userSettingsResource: URI.file(environmentService.appSettingsPath), remoteAuthority: this.configuration.remoteAuthority, configurationCache: new ConfigurationCache(environmentService) }, configurationFileService, remoteAgentService); - return workspaceService.initialize(payload).then(() => workspaceService, error => { + try { + await workspaceService.initialize(payload); + + return workspaceService; + } catch (error) { onUnexpectedError(error); logService.error(error); return workspaceService; - }); + } } - private createStorageService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, logService: ILogService, mainProcessService: IMainProcessService): Promise { + private async createStorageService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, logService: ILogService, mainProcessService: IMainProcessService): Promise { const globalStorageDatabase = new GlobalStorageDatabaseChannelClient(mainProcessService.getChannel('storage')); const storageService = new StorageService(globalStorageDatabase, logService, environmentService); - return storageService.initialize(payload).then(() => storageService, error => { + try { + await storageService.initialize(payload); + + return storageService; + } catch (error) { onUnexpectedError(error); logService.error(error); return storageService; - }); + } } private createLogService(mainProcessService: IMainProcessService, environmentService: IWorkbenchEnvironmentService): ILogService { diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 3155e8ccfe..068ced2e25 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -117,7 +117,7 @@ export class ElectronWindow extends Disposable { }); // Support runAction event - ipc.on('vscode:runAction', (event: Event, request: IRunActionInWindowRequest) => { + ipc.on('vscode:runAction', async (event: Event, request: IRunActionInWindowRequest) => { const args: unknown[] = request.args || []; // If we run an action from the touchbar, we fill in the currently active resource @@ -134,7 +134,9 @@ export class ElectronWindow extends Disposable { args.push({ from: request.from }); // TODO@telemetry this is a bit weird to send this to every action? } - this.commandService.executeCommand(request.id, ...args).then(_ => { + try { + await this.commandService.executeCommand(request.id, ...args); + /* __GDPR__ "commandExecuted" : { "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -142,9 +144,9 @@ export class ElectronWindow extends Disposable { } */ this.telemetryService.publicLog('commandExecuted', { id: request.id, from: request.from }); - }, err => { - this.notificationService.error(err); - }); + } catch (error) { + this.notificationService.error(error); + } }); // Support runKeybinding event @@ -173,34 +175,30 @@ export class ElectronWindow extends Disposable { }); // Fullscreen Events - ipc.on('vscode:enterFullScreen', () => { - this.lifecycleService.when(LifecyclePhase.Ready).then(() => { - browser.setFullscreen(true); - }); + ipc.on('vscode:enterFullScreen', async () => { + await this.lifecycleService.when(LifecyclePhase.Ready); + browser.setFullscreen(true); }); - ipc.on('vscode:leaveFullScreen', () => { - this.lifecycleService.when(LifecyclePhase.Ready).then(() => { - browser.setFullscreen(false); - }); + ipc.on('vscode:leaveFullScreen', async () => { + await this.lifecycleService.when(LifecyclePhase.Ready); + browser.setFullscreen(false); }); // High Contrast Events - ipc.on('vscode:enterHighContrast', () => { + ipc.on('vscode:enterHighContrast', async () => { const windowConfig = this.configurationService.getValue('window'); if (windowConfig && windowConfig.autoDetectHighContrast) { - this.lifecycleService.when(LifecyclePhase.Ready).then(() => { - this.themeService.setColorTheme(VS_HC_THEME, undefined); - }); + await this.lifecycleService.when(LifecyclePhase.Ready); + this.themeService.setColorTheme(VS_HC_THEME, undefined); } }); - ipc.on('vscode:leaveHighContrast', () => { + ipc.on('vscode:leaveHighContrast', async () => { const windowConfig = this.configurationService.getValue('window'); if (windowConfig && windowConfig.autoDetectHighContrast) { - this.lifecycleService.when(LifecyclePhase.Ready).then(() => { - this.themeService.restoreColorTheme(); - }); + await this.lifecycleService.when(LifecyclePhase.Ready); + this.themeService.restoreColorTheme(); } }); @@ -310,32 +308,28 @@ export class ElectronWindow extends Disposable { }; // Emit event when vscode is ready - this.lifecycleService.when(LifecyclePhase.Ready).then(() => { - ipc.send('vscode:workbenchReady', this.windowService.windowId); - }); + this.lifecycleService.when(LifecyclePhase.Ready).then(() => ipc.send('vscode:workbenchReady', this.windowService.windowId)); // Integrity warning this.integrityService.isPure().then(res => this.titleService.updateProperties({ isPure: res.isPure })); // Root warning - this.lifecycleService.when(LifecyclePhase.Restored).then(() => { - let isAdminPromise: Promise; + this.lifecycleService.when(LifecyclePhase.Restored).then(async () => { + let isAdmin: boolean; if (isWindows) { - isAdminPromise = import('native-is-elevated').then(isElevated => isElevated()); + const isElevated = await import('native-is-elevated'); + isAdmin = isElevated(); } else { - isAdminPromise = Promise.resolve(isRootUser()); + isAdmin = isRootUser(); } - return isAdminPromise.then(isAdmin => { + // Update title + this.titleService.updateProperties({ isAdmin }); - // Update title - this.titleService.updateProperties({ isAdmin }); - - // Show warning message (unix only) - if (isAdmin && !isWindows) { - this.notificationService.warn(nls.localize('runningAsRoot', "It is not recommended to run {0} as root user.", product.nameShort)); - } - }); + // Show warning message (unix only) + if (isAdmin && !isWindows) { + this.notificationService.warn(nls.localize('runningAsRoot', "It is not recommended to run {0} as root user.", product.nameShort)); + } }); // Touchbar menu (if enabled) @@ -408,7 +402,7 @@ export class ElectronWindow extends Disposable { } } - private setupCrashReporter(): void { + private async setupCrashReporter(): Promise { // base options with product info const options = { @@ -422,18 +416,14 @@ export class ElectronWindow extends Disposable { }; // mixin telemetry info - this.telemetryService.getTelemetryInfo() - .then(info => { - assign(options.extra, { - vscode_sessionId: info.sessionId - }); + const info = await this.telemetryService.getTelemetryInfo(); + assign(options.extra, { vscode_sessionId: info.sessionId }); - // start crash reporter right here - crashReporter.start(deepClone(options)); + // start crash reporter right here + crashReporter.start(deepClone(options)); - // start crash reporter in the main process - return this.windowsService.startCrashReporter(options); - }); + // start crash reporter in the main process + return this.windowsService.startCrashReporter(options); } private onAddFoldersRequest(request: IAddFoldersRequest): void { @@ -525,22 +515,21 @@ export class ElectronWindow extends Disposable { }); } - private openResources(resources: Array, diffMode: boolean): void { - this.lifecycleService.when(LifecyclePhase.Ready).then((): Promise => { + private async openResources(resources: Array, diffMode: boolean): Promise { + await this.lifecycleService.when(LifecyclePhase.Ready); - // In diffMode we open 2 resources as diff - if (diffMode && resources.length === 2) { - return this.editorService.openEditor({ leftResource: resources[0].resource!, rightResource: resources[1].resource!, options: { pinned: true } }); - } + // In diffMode we open 2 resources as diff + if (diffMode && resources.length === 2) { + return this.editorService.openEditor({ leftResource: resources[0].resource!, rightResource: resources[1].resource!, options: { pinned: true } }); + } - // For one file, just put it into the current active editor - if (resources.length === 1) { - return this.editorService.openEditor(resources[0]); - } + // For one file, just put it into the current active editor + if (resources.length === 1) { + return this.editorService.openEditor(resources[0]); + } - // Otherwise open all - return this.editorService.openEditors(resources); - }); + // Otherwise open all + return this.editorService.openEditors(resources); } dispose(): void { diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index 9dc5fa055d..01ae4ffdab 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -12,7 +12,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; import { ConfigurationModel, ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels'; -import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, IConfigurationFileService, MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; +import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, IConfigurationFileService, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; import { IStoredWorkspaceFolder, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService'; import { WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -45,7 +45,7 @@ export class RemoteUserConfiguration extends Disposable { this._userConfiguration = this._cachedConfiguration = new CachedUserConfiguration(remoteAuthority, configurationCache); remoteAgentService.getEnvironment().then(async environment => { if (environment) { - const userConfiguration = this._register(new UserConfiguration(environment.settingsPath, MACHINE_SCOPES, this._configurationFileService)); + const userConfiguration = this._register(new UserConfiguration(environment.settingsPath, REMOTE_MACHINE_SCOPES, this._configurationFileService)); this._register(userConfiguration.onDidChangeConfiguration(configurationModel => this.onDidUserConfigurationChange(configurationModel))); this._userConfigurationInitializationPromise = userConfiguration.initialize(); const configurationModel = await this._userConfigurationInitializationPromise; diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 398540b1e0..77f59f88bb 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -15,7 +15,7 @@ import { isLinux } from 'vs/base/common/platform'; import { ConfigurationChangeEvent, ConfigurationModel, DefaultConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, keyFromOverrideIdentifier, isConfigurationOverrides, IConfigurationData, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Configuration, WorkspaceConfigurationChangeEvent, AllKeysConfigurationChangeEvent } from 'vs/workbench/services/configuration/common/configurationModels'; -import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, IConfigurationFileService, machineSettingsSchemaId } from 'vs/workbench/services/configuration/common/configuration'; +import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, IConfigurationFileService, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings, machineSettings } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceInitializationPayload, isSingleFolderWorkspaceInitializationPayload, ISingleFolderWorkspaceInitializationPayload, IEmptyWorkspaceInitializationPayload, useSlashForPath, getStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; @@ -74,7 +74,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic this.defaultConfiguration = new DefaultConfigurationModel(); this.configurationCache = configurationCache; if (userSettingsResource) { - this.localUserConfiguration = this._register(new UserConfiguration(userSettingsResource, undefined, configurationFileService)); + this.localUserConfiguration = this._register(new UserConfiguration(userSettingsResource, remoteAuthority ? LOCAL_MACHINE_SCOPES : undefined, configurationFileService)); this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration))); } if (remoteAuthority) { @@ -515,14 +515,16 @@ export class WorkspaceService extends Disposable implements IConfigurationServic }, {}); }; + const unsupportedApplicationSettings = convertToNotSuggestedProperties(applicationSettings.properties, localize('unsupportedApplicationSetting', "This setting can be applied only in application user settings")); + const unsupportedMachineSettings = convertToNotSuggestedProperties(machineSettings.properties, localize('unsupportedMachineSetting', "This setting can be applied only in user settings")); + const unsupportedRemoteMachineSettings = convertToNotSuggestedProperties(machineSettings.properties, localize('unsupportedRemoteMachineSetting', "This setting can be applied only in remote machine settings")); const allSettingsSchema: IJSONSchema = { properties: allSettings.properties, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; - const unsupportedApplicationSettings = convertToNotSuggestedProperties(applicationSettings.properties, localize('unsupportedApplicationSetting', "This setting can be applied only in application user Settings")); - const unsupportedMachineSettings = convertToNotSuggestedProperties(machineSettings.properties, localize('unsupportedMachineSetting', "This setting can be applied only in user Settings")); + const userSettingsSchema: IJSONSchema = this.remoteUserConfiguration ? { properties: { ...applicationSettings.properties, ...unsupportedRemoteMachineSettings, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' } : allSettingsSchema; const machineSettingsSchema: IJSONSchema = { properties: { ...unsupportedApplicationSettings, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; const workspaceSettingsSchema: IJSONSchema = { properties: { ...unsupportedApplicationSettings, ...unsupportedMachineSettings, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' }; jsonRegistry.registerSchema(defaultSettingsSchemaId, allSettingsSchema); - jsonRegistry.registerSchema(userSettingsSchemaId, allSettingsSchema); + jsonRegistry.registerSchema(userSettingsSchemaId, userSettingsSchema); jsonRegistry.registerSchema(machineSettingsSchemaId, machineSettingsSchema); if (WorkbenchState.WORKSPACE === this.getWorkbenchState()) { diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index 79e4f8169d..543506e415 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -20,7 +20,8 @@ export const workspaceSettingsSchemaId = 'vscode://schemas/settings/workspace'; export const folderSettingsSchemaId = 'vscode://schemas/settings/folder'; export const launchSchemaId = 'vscode://schemas/launch'; -export const MACHINE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]; +export const LOCAL_MACHINE_SCOPES = [ConfigurationScope.APPLICATION, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]; +export const REMOTE_MACHINE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]; export const WORKSPACE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]; export const FOLDER_SCOPES = [ConfigurationScope.RESOURCE]; diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index e54e5e83d3..6c0b3a728d 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -268,6 +268,14 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { // return promise; // }); + test('machine settings in local user settings does not override defaults', async () => { + fs.writeFileSync(globalSettingsFile, '{ "configurationService.remote.machineSetting": "globalValue" }'); + registerRemoteFileSystemProvider(); + resolveRemoteEnvironment(); + await initialize(); + assert.equal(testObject.getValue('configurationService.remote.machineSetting'), 'isSet'); + }); + }); function getWorkspaceId(configPath: URI): string { diff --git a/src/vs/workbench/services/contextmenu/electron-browser/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-browser/contextmenuService.ts index ef7f0fe4ea..f3a66bf4e4 100644 --- a/src/vs/workbench/services/contextmenu/electron-browser/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-browser/contextmenuService.ts @@ -172,7 +172,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService } } - private runAction(actionRunner: IActionRunner, actionToRun: IAction, delegate: IContextMenuDelegate, event: IContextMenuEvent): void { + private async runAction(actionRunner: IActionRunner, actionToRun: IAction, delegate: IContextMenuDelegate, event: IContextMenuEvent): Promise { /* __GDPR__ "workbenchActionExecuted" : { "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -182,9 +182,15 @@ class NativeContextMenuService extends Disposable implements IContextMenuService this.telemetryService.publicLog('workbenchActionExecuted', { id: actionToRun.id, from: 'contextMenu' }); const context = delegate.getActionsContext ? delegate.getActionsContext(event) : event; - const res = actionRunner.run(actionToRun, context) || Promise.resolve(null); - res.then(undefined, e => this.notificationService.error(e)); + const runnable = actionRunner.run(actionToRun, context); + if (runnable) { + try { + await runnable; + } catch (error) { + this.notificationService.error(error); + } + } } } diff --git a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts index 83f368630c..2f2e7c67e3 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts @@ -78,17 +78,16 @@ class NativeDialogService implements IDialogService { sharedProcessService.registerChannel('dialog', new DialogChannel(this)); } - confirm(confirmation: IConfirmation): Promise { + async confirm(confirmation: IConfirmation): Promise { this.logService.trace('DialogService#confirm', confirmation.message); const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation)); - return this.windowService.showMessageBox(options).then(result => { - return { - confirmed: buttonIndexMap[result.button] === 0 ? true : false, - checkboxChecked: result.checkboxChecked - }; - }); + const result = await this.windowService.showMessageBox(options); + return { + confirmed: buttonIndexMap[result.button] === 0 ? true : false, + checkboxChecked: result.checkboxChecked + }; } private getConfirmOptions(confirmation: IConfirmation): Electron.MessageBoxOptions { @@ -128,7 +127,7 @@ class NativeDialogService implements IDialogService { return opts; } - show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): Promise { + async show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): Promise { this.logService.trace('DialogService#show', message); const { options, buttonIndexMap } = this.massageMessageBoxOptions({ @@ -139,7 +138,8 @@ class NativeDialogService implements IDialogService { detail: dialogOptions ? dialogOptions.detail : undefined }); - return this.windowService.showMessageBox(options).then(result => buttonIndexMap[result.button]); + const result = await this.windowService.showMessageBox(options); + return buttonIndexMap[result.button]; } private massageMessageBoxOptions(options: Electron.MessageBoxOptions): IMassagedMessageBoxOptions { diff --git a/src/vs/workbench/services/editor/browser/codeEditorService.ts b/src/vs/workbench/services/editor/browser/codeEditorService.ts index 2606455bc4..75e4c9416c 100644 --- a/src/vs/workbench/services/editor/browser/codeEditorService.ts +++ b/src/vs/workbench/services/editor/browser/codeEditorService.ts @@ -62,17 +62,16 @@ export class CodeEditorService extends CodeEditorServiceImpl { return this.doOpenCodeEditor(input, source, sideBySide); } - private doOpenCodeEditor(input: IResourceInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { - return this.editorService.openEditor(input, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(control => { - if (control) { - const widget = control.getControl(); - if (isCodeEditor(widget)) { - return widget; - } + private async doOpenCodeEditor(input: IResourceInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { + const control = await this.editorService.openEditor(input, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + if (control) { + const widget = control.getControl(); + if (isCodeEditor(widget)) { + return widget; } + } - return null; - }); + return null; } } diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index ca5cb87144..e7a83af351 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -331,7 +331,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { openEditors(editors: IEditorInputWithOptions[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; openEditors(editors: IResourceEditor[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; - openEditors(editors: Array, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise { + async openEditors(editors: Array, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise { // Convert to typed editors and options const typedEditors: IEditorInputWithOptions[] = []; @@ -367,7 +367,9 @@ export class EditorService extends Disposable implements EditorServiceImpl { result.push(group.openEditors(editorsWithOptions)); }); - return Promise.all(result).then(editors => coalesce(editors)); + const openedEditors = await Promise.all(result); + + return coalesce(openedEditors); } //#endregion diff --git a/src/vs/workbench/services/extensions/node/extensionsUtil.ts b/src/vs/workbench/services/extensions/node/extensionsUtil.ts index 935cdaceca..8fe148d902 100644 --- a/src/vs/workbench/services/extensions/node/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/node/extensionsUtil.ts @@ -6,9 +6,48 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { isUIExtension as _isUIExtension } from 'vs/platform/extensions/node/extensionsUtil'; +import { getGalleryExtensionId, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; +import product from 'vs/platform/product/node/product'; export function isUIExtension(manifest: IExtensionManifest, configurationService: IConfigurationService): boolean { - const uiExtensionPoints = ExtensionsRegistry.getExtensionPoints().filter(e => e.defaultExtensionKind !== 'workspace').map(e => e.name); - return _isUIExtension(manifest, uiExtensionPoints, configurationService); + const uiContributions = ExtensionsRegistry.getExtensionPoints().filter(e => e.defaultExtensionKind !== 'workspace').map(e => e.name); + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); + const extensionKind = getExtensionKind(manifest, configurationService); + switch (extensionKind) { + case 'ui': return true; + case 'workspace': return false; + default: { + // Tagged as UI extension in product + if (isNonEmptyArray(product.uiExtensions) && product.uiExtensions.some(id => areSameExtensions({ id }, { id: extensionId }))) { + return true; + } + // Not an UI extension if it has main + if (manifest.main) { + return false; + } + // Not an UI extension if it has dependencies or an extension pack + if (isNonEmptyArray(manifest.extensionDependencies) || isNonEmptyArray(manifest.extensionPack)) { + return false; + } + if (manifest.contributes) { + // Not an UI extension if it has no ui contributions + if (!uiContributions.length || Object.keys(manifest.contributes).some(contribution => uiContributions.indexOf(contribution) === -1)) { + return false; + } + } + return true; + } + } +} + +function getExtensionKind(manifest: IExtensionManifest, configurationService: IConfigurationService): string | undefined { + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); + const configuredExtensionKinds = configurationService.getValue<{ [key: string]: string }>('remote.extensionKind') || {}; + for (const id of Object.keys(configuredExtensionKinds)) { + if (areSameExtensions({ id: extensionId }, { id })) { + return configuredExtensionKinds[id]; + } + } + return manifest.extensionKind; } diff --git a/src/vs/workbench/services/files/node/watcher/nodejs/watcherService.ts b/src/vs/workbench/services/files/node/watcher/nodejs/watcherService.ts index 73fe914c66..1b815fd92f 100644 --- a/src/vs/workbench/services/files/node/watcher/nodejs/watcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/nodejs/watcherService.ts @@ -5,7 +5,8 @@ import { IDiskFileChange, normalizeFileChanges } from 'vs/workbench/services/files/node/watcher/watcher'; import { Disposable } from 'vs/base/common/lifecycle'; -import { statLink, readlink } from 'vs/base/node/pfs'; +import { statLink } from 'vs/base/node/pfs'; +import { realpath } from 'vs/base/node/extpath'; import { watchFolder, watchFile, CHANGE_BUFFER_DELAY } from 'vs/base/node/watcher'; import { FileChangeType } from 'vs/platform/files/common/files'; import { ThrottledDelayer } from 'vs/base/common/async'; @@ -40,7 +41,7 @@ export class FileWatcher extends Disposable { let pathToWatch = this.path; if (isSymbolicLink) { try { - pathToWatch = await readlink(pathToWatch); + pathToWatch = await realpath(pathToWatch); } catch (error) { this.onError(error); } diff --git a/src/vs/workbench/services/integrity/node/integrityService.ts b/src/vs/workbench/services/integrity/node/integrityService.ts index 3971a51959..5943f1ba14 100644 --- a/src/vs/workbench/services/integrity/node/integrityService.ts +++ b/src/vs/workbench/services/integrity/node/integrityService.ts @@ -71,9 +71,9 @@ export class IntegrityServiceImpl implements IIntegrityService { this.isPure().then(r => { if (r.isPure) { - // all is good - return; + return; // all is good } + this._prompt(); }); } @@ -106,29 +106,25 @@ export class IntegrityServiceImpl implements IIntegrityService { return this._isPurePromise; } - private _isPure(): Promise { + private async _isPure(): Promise { const expectedChecksums = product.checksums || {}; - return this.lifecycleService.when(LifecyclePhase.Eventually).then(() => { - let asyncResults: Promise[] = Object.keys(expectedChecksums).map((filename) => { - return this._resolve(filename, expectedChecksums[filename]); - }); + await this.lifecycleService.when(LifecyclePhase.Eventually); - return Promise.all(asyncResults).then((allResults) => { - let isPure = true; - for (let i = 0, len = allResults.length; i < len; i++) { - if (!allResults[i].isPure) { - isPure = false; - break; - } - } + const allResults = await Promise.all(Object.keys(expectedChecksums).map(filename => this._resolve(filename, expectedChecksums[filename]))); - return { - isPure: isPure, - proof: allResults - }; - }); - }); + let isPure = true; + for (let i = 0, len = allResults.length; i < len; i++) { + if (!allResults[i].isPure) { + isPure = false; + break; + } + } + + return { + isPure: isPure, + proof: allResults + }; } private _resolve(filename: string, expected: string): Promise { diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index e46c10f3f4..f5f09c13ab 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -208,7 +208,8 @@ export class ScopedProgressService extends ScopedService implements IProgressSer }; } - showWhile(promise: Promise, delay?: number): Promise { + async showWhile(promise: Promise, delay?: number): Promise { + // Join with existing running promise to ensure progress is accurate if (this.progressState.type === ProgressState.Type.While) { promise = Promise.all([promise, this.progressState.whilePromise]); @@ -217,24 +218,25 @@ export class ScopedProgressService extends ScopedService implements IProgressSer // Keep Promise in State this.progressState = new ProgressState.While(promise, delay || 0, Date.now()); - let stop = () => { + try { + this.doShowWhile(delay); - // If this is not the last promise in the list of joined promises, return early - if (this.progressState.type === ProgressState.Type.While && this.progressState.whilePromise !== promise) { - return; + await promise; + } catch (error) { + // ignore + } finally { + + // If this is not the last promise in the list of joined promises, skip this + if (this.progressState.type !== ProgressState.Type.While || this.progressState.whilePromise === promise) { + + // The while promise is either null or equal the promise we last hooked on + this.progressState = ProgressState.None; + + if (this.isActive) { + this.progressbar.stop().hide(); + } } - - // The while promise is either null or equal the promise we last hooked on - this.progressState = ProgressState.None; - - if (this.isActive) { - this.progressbar.stop().hide(); - } - }; - - this.doShowWhile(delay); - - return promise.then(stop, stop); + } } private doShowWhile(delay?: number): void { @@ -280,13 +282,15 @@ export class ProgressService implements IProgressService { }; } - showWhile(promise: Promise, delay?: number): Promise { - const stop = () => { + async showWhile(promise: Promise, delay?: number): Promise { + try { + this.progressbar.infinite().show(delay); + + await promise; + } catch (error) { + // ignore + } finally { this.progressbar.stop().hide(); - }; - - this.progressbar.infinite().show(delay); - - return promise.then(stop, stop); + } } } diff --git a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts index 9938857805..6e6b3fa7fa 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts @@ -34,28 +34,28 @@ export class RemoteExtensionEnvironmentChannelClient { constructor(private channel: IChannel) { } - getEnvironmentData(remoteAuthority: string, extensionDevelopmentPath?: URI[]): Promise { + async getEnvironmentData(remoteAuthority: string, extensionDevelopmentPath?: URI[]): Promise { const args: IGetEnvironmentDataArguments = { language: platform.language, remoteAuthority, extensionDevelopmentPath }; - return this.channel.call('getEnvironmentData', args) - .then((data: IRemoteAgentEnvironmentDTO): IRemoteAgentEnvironment => { - return { - pid: data.pid, - appRoot: URI.revive(data.appRoot), - appSettingsHome: URI.revive(data.appSettingsHome), - settingsPath: URI.revive(data.settingsPath), - logsPath: URI.revive(data.logsPath), - extensionsPath: URI.revive(data.extensionsPath), - extensionHostLogsPath: URI.revive(data.extensionHostLogsPath), - globalStorageHome: URI.revive(data.globalStorageHome), - userHome: URI.revive(data.userHome), - extensions: data.extensions.map(ext => { (ext).extensionLocation = URI.revive(ext.extensionLocation); return ext; }), - os: data.os - }; - }); + + const data = await this.channel.call('getEnvironmentData', args); + + return { + pid: data.pid, + appRoot: URI.revive(data.appRoot), + appSettingsHome: URI.revive(data.appSettingsHome), + settingsPath: URI.revive(data.settingsPath), + logsPath: URI.revive(data.logsPath), + extensionsPath: URI.revive(data.extensionsPath), + extensionHostLogsPath: URI.revive(data.extensionHostLogsPath), + globalStorageHome: URI.revive(data.globalStorageHome), + userHome: URI.revive(data.userHome), + extensions: data.extensions.map(ext => { (ext).extensionLocation = URI.revive(ext.extensionLocation); return ext; }), + os: data.os + }; } getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise { diff --git a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts index 3bd26b9f75..340bb88cf9 100644 --- a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts +++ b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts @@ -31,7 +31,7 @@ class ResourceModelCollection extends ReferenceCollection { + async createReferencedObject(key: string, skipActivateProvider?: boolean): Promise { this.modelsToDispose.delete(key); const resource = URI.parse(key); @@ -43,15 +43,19 @@ class ResourceModelCollection extends ReferenceCollection this.instantiationService.createInstance(ResourceEditorModel, resource)); + await this.resolveTextModelContent(key); + + return this.instantiationService.createInstance(ResourceEditorModel, resource); } // Either unknown schema, or not yet registered, try to activate if (!skipActivateProvider) { - return this.fileService.activateProvider(resource.scheme).then(() => this.createReferencedObject(key, true)); + await this.fileService.activateProvider(resource.scheme); + + return this.createReferencedObject(key, true); } - return Promise.reject(new Error('resource is not available')); + throw new Error('resource is not available'); } destroyReferencedObject(key: string, modelPromise: Promise): void { @@ -101,18 +105,17 @@ class ResourceModelCollection extends ReferenceCollection { + private async resolveTextModelContent(key: string): Promise { const resource = URI.parse(key); const providers = this.providers[resource.scheme] || []; const factories = providers.map(p => () => Promise.resolve(p.provideTextContent(resource))); - return first(factories).then(model => { - if (!model) { - return Promise.reject(new Error('resource is not available')); - } + const model = await first(factories); + if (!model) { + throw new Error('resource is not available'); + } - return model; - }); + return model; } } @@ -131,14 +134,16 @@ export class TextModelResolverService implements ITextModelService { } createModelReference(resource: URI): Promise> { - return this._createModelReference(resource); + return this.doCreateModelReference(resource); } - private _createModelReference(resource: URI): Promise> { + private async doCreateModelReference(resource: URI): Promise> { // Untitled Schema: go through cached input if (resource.scheme === network.Schemas.untitled) { - return this.untitledEditorService.loadOrCreate({ resource }).then(model => new ImmortalReference(model as IResolvedTextEditorModel)); + const model = await this.untitledEditorService.loadOrCreate({ resource }); + + return new ImmortalReference(model as IResolvedTextEditorModel); } // InMemory Schema: go through model service cache @@ -154,14 +159,15 @@ export class TextModelResolverService implements ITextModelService { const ref = this.resourceModelCollection.acquire(resource.toString()); - return ref.object.then( - model => ({ object: model, dispose: () => ref.dispose() }), - err => { - ref.dispose(); + try { + const model = await ref.object; - return Promise.reject(err); - } - ); + return { object: model as IResolvedTextEditorModel, dispose: () => ref.dispose() }; + } catch (error) { + ref.dispose(); + + throw error; + } } registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable { diff --git a/src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts b/src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts index 741cfa8884..5d1f94806c 100644 --- a/src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts @@ -71,6 +71,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { if (reason !== ShutdownReason.LOAD && reason !== ShutdownReason.CLOSE) { return undefined; // only interested when window is closing or loading } + const workspaceIdentifier = this.getCurrentWorkspaceIdentifier(); if (!workspaceIdentifier || !isEqualOrParent(workspaceIdentifier.configPath, this.environmentService.untitledWorkspacesHome)) { return undefined; // only care about untitled workspaces to ask for saving @@ -190,16 +191,23 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { } } - private doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToDelete: URI[], index?: number, donotNotifyError: boolean = false): Promise { - return this.contextService.updateFolders(foldersToAdd, foldersToDelete, index) - .then(() => null, error => donotNotifyError ? Promise.reject(error) : this.handleWorkspaceConfigurationEditingError(error)); + private async doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToDelete: URI[], index?: number, donotNotifyError: boolean = false): Promise { + try { + await this.contextService.updateFolders(foldersToAdd, foldersToDelete, index); + } catch (error) { + if (donotNotifyError) { + throw error; + } + + this.handleWorkspaceConfigurationEditingError(error); + } } addFolders(foldersToAdd: IWorkspaceFolderCreationData[], donotNotifyError: boolean = false): Promise { return this.doAddFolders(foldersToAdd, undefined, donotNotifyError); } - private doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number, donotNotifyError: boolean = false): Promise { + private async doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number, donotNotifyError: boolean = false): Promise { const state = this.contextService.getWorkbenchState(); // If we are in no-workspace or single-folder workspace, adding folders has to @@ -217,11 +225,18 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { } // Delegate addition of folders to workspace service otherwise - return this.contextService.addFolders(foldersToAdd, index) - .then(() => null, error => donotNotifyError ? Promise.reject(error) : this.handleWorkspaceConfigurationEditingError(error)); + try { + await this.contextService.addFolders(foldersToAdd, index); + } catch (error) { + if (donotNotifyError) { + throw error; + } + + this.handleWorkspaceConfigurationEditingError(error); + } } - removeFolders(foldersToRemove: URI[], donotNotifyError: boolean = false): Promise { + async removeFolders(foldersToRemove: URI[], donotNotifyError: boolean = false): Promise { // If we are in single-folder state and the opened folder is to be removed, // we create an empty workspace and enter it. @@ -230,8 +245,15 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { } // Delegate removal of folders to workspace service otherwise - return this.contextService.removeFolders(foldersToRemove) - .then(() => null, error => donotNotifyError ? Promise.reject(error) : this.handleWorkspaceConfigurationEditingError(error)); + try { + await this.contextService.removeFolders(foldersToRemove); + } catch (error) { + if (donotNotifyError) { + throw error; + } + + this.handleWorkspaceConfigurationEditingError(error); + } } private includesSingleFolderWorkspace(folders: URI[]): boolean { @@ -283,10 +305,12 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { detail: nls.localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."), noLink: true }; - return this.windowService.showMessageBox(options).then(() => false); + await this.windowService.showMessageBox(options); + + return false; } - return Promise.resolve(true); // OK + return true; // OK } private async saveWorkspaceAs(workspace: IWorkspaceIdentifier, targetConfigPathURI: URI): Promise {