diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 09c30b0fd0e..5c5b079b7ff 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -383,7 +383,7 @@ export function sequence(promiseFactory: ITask>[]): TPromise } function thenHandler(result: any): Promise { - if (result) { + if (result !== undefined && result !== null) { results.push(result); } diff --git a/src/vs/code/electron-main/launch.ts b/src/vs/code/electron-main/launch.ts index d2485997bd0..99678232b30 100644 --- a/src/vs/code/electron-main/launch.ts +++ b/src/vs/code/electron-main/launch.ts @@ -56,7 +56,7 @@ export class LaunchService implements ILaunchService { ) {} start(args: ICommandLineArguments, userEnv: IProcessEnvironment): TPromise { - this.logService.log('Received data from other instance', args, userEnv); + this.logService.log('Received data from other instance: ', args, userEnv); // Otherwise handle in windows service let usedWindows: VSCodeWindow[]; diff --git a/src/vs/code/electron-main/log.ts b/src/vs/code/electron-main/log.ts index c281e1a8e38..352363d63ca 100644 --- a/src/vs/code/electron-main/log.ts +++ b/src/vs/code/electron-main/log.ts @@ -26,7 +26,7 @@ export class MainLogService implements ILogService { const { verbose } = this.envService.cliArgs; if (verbose) { - console.log(`(${new Date().toLocaleTimeString()})`, ...args); + console.log(`\x1b[93m[main ${new Date().toLocaleTimeString()}]\x1b[0m`, ...args); } } } \ No newline at end of file diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 3a15129ab3b..122121e8d6a 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -99,8 +99,9 @@ function main(accessor: ServicesAccessor, mainIpcServer: Server, userEnv: IProce } }); - logService.log('### VSCode main.js ###'); - logService.log(envService.appRoot, envService.cliArgs); + logService.log('Starting VS Code in verbose mode'); + logService.log(`from: ${envService.appRoot}`); + logService.log('args:', envService.cliArgs); // Setup Windows mutex let windowsMutex: Mutex = null; diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 18e6d60018c..7097b5db30e 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -41,12 +41,18 @@ export function main(argv: string[]): TPromise { 'VSCODE_CLI': '1', 'ELECTRON_NO_ATTACH_CONSOLE': '1' }); + delete env['ELECTRON_RUN_AS_NODE']; - let options = { + if (args.verbose) { + env['ELECTRON_ENABLE_LOGGING'] = '1'; + } + + const options = { detached: true, env, }; + if (!args.verbose) { options['stdio'] = 'ignore'; } diff --git a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts index 1d18c8709d9..e8ebd24c472 100644 --- a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts @@ -16,19 +16,26 @@ import {fromRange} from 'vs/workbench/api/node/extHostTypeConverters'; import {IResourceEdit} from 'vs/editor/common/services/bulkEdit'; import {ExtHostDocuments} from 'vs/workbench/api/node/extHostDocuments'; +declare class WeakMap { + // delete(key: K): boolean; + get(key: K): V; + // has(key: K): boolean; + set(key: K, value?: V): WeakMap; +} export class ExtHostDocumentSaveParticipant extends ExtHostDocumentSaveParticipantShape { private _documents: ExtHostDocuments; private _workspace: MainThreadWorkspaceShape; - private _listenerTimeout: number; private _callbacks = new CallbackList(); + private _badListeners = new WeakMap(); + private _thresholds: { timeout: number; errors: number; }; - constructor(documents: ExtHostDocuments, workspace: MainThreadWorkspaceShape, listenerTimeout: number = 1000) { + constructor(documents: ExtHostDocuments, workspace: MainThreadWorkspaceShape, thresholds: { timeout: number; errors: number; } = { timeout: 1000, errors: 5 }) { super(); this._documents = documents; this._workspace = workspace; - this._listenerTimeout = listenerTimeout; + this._thresholds = thresholds; } dispose(): void { @@ -38,7 +45,11 @@ export class ExtHostDocumentSaveParticipant extends ExtHostDocumentSaveParticipa get onWillSaveTextDocumentEvent(): Event { return (listener, thisArg, disposables) => { this._callbacks.add(listener, thisArg); - const result = { dispose: () => this._callbacks.remove(listener, thisArg) }; + const result = { + dispose: () => { + this._callbacks.remove(listener, thisArg); + } + }; if (Array.isArray(disposables)) { disposables.push(result); } @@ -46,13 +57,35 @@ export class ExtHostDocumentSaveParticipant extends ExtHostDocumentSaveParticipa }; } - $participateInSave(resource: URI): TPromise { + $participateInSave(resource: URI): TPromise { const entries = this._callbacks.entries(); return sequence(entries.map(([fn, thisArg]) => { return () => { + + const errors = this._badListeners.get(fn); + if (errors > this._thresholds.errors) { + // ignored + return TPromise.wrap(false); + } + const document = this._documents.getDocumentData(resource).document; - return this._deliverEventAsync(fn, thisArg, document); + return this._deliverEventAsync(fn, thisArg, document).then(() => { + // don't send result across the wire + return true; + + }, err => { + if (!(err instanceof Error) || (err).message !== 'concurrent_edits') { + const errors = this._badListeners.get(fn); + this._badListeners.set(fn, !errors ? 1 : errors + 1); + + // todo@joh signal to the listener? + // if (errors === this._thresholds.errors) { + // console.warn('BAD onWillSaveTextDocumentEvent-listener is from now on being ignored'); + // } + } + return false; + }); }; })); } @@ -77,7 +110,7 @@ export class ExtHostDocumentSaveParticipant extends ExtHostDocumentSaveParticipa // fire event listener.apply(thisArg, [event]); } catch (err) { - return TPromise.as(new Error(err)); + return TPromise.wrapError(err); } // freeze promises after event call @@ -85,7 +118,7 @@ export class ExtHostDocumentSaveParticipant extends ExtHostDocumentSaveParticipa return new TPromise((resolve, reject) => { // join on all listener promises, reject after timeout - const handle = setTimeout(() => reject(new Error('timeout')), this._listenerTimeout); + const handle = setTimeout(() => reject(new Error('timeout')), this._thresholds.timeout); return always(TPromise.join(promises), () => clearTimeout(handle)).then(resolve, reject); }).then(values => { @@ -114,11 +147,7 @@ export class ExtHostDocumentSaveParticipant extends ExtHostDocumentSaveParticipa } // TODO@joh bubble this to listener? - return new Error('ignoring change because of concurrent edits'); - - }, err => { - // soft ignore, turning into result - return err; + return TPromise.wrapError(new Error('concurrent_edits')); }); } } \ No newline at end of file diff --git a/src/vs/workbench/api/node/mainThreadSaveParticipant.ts b/src/vs/workbench/api/node/mainThreadSaveParticipant.ts index c6af4737f32..a2b4de39275 100644 --- a/src/vs/workbench/api/node/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/node/mainThreadSaveParticipant.ts @@ -5,47 +5,33 @@ 'use strict'; -import {IDisposable, dispose} from 'vs/base/common/lifecycle'; import {TPromise} from 'vs/base/common/winjs.base'; +import {sequence} from 'vs/base/common/async'; import {ICodeEditorService} from 'vs/editor/common/services/codeEditorService'; import {IThreadService} from 'vs/workbench/services/thread/common/threadService'; import {ISaveParticipant, ITextFileEditorModel} from 'vs/workbench/parts/files/common/files'; -import {IFilesConfiguration} from 'vs/platform/files/common/files'; -import {IPosition, IModel} from 'vs/editor/common/editorCommon'; +import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; +import {IPosition, IModel, ICommonCodeEditor, ISingleEditOperation} from 'vs/editor/common/editorCommon'; +import {Range} from 'vs/editor/common/core/range'; import {Selection} from 'vs/editor/common/core/selection'; import {trimTrailingWhitespace} from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; +import {getDocumentRangeFormattingEdits} from 'vs/editor/contrib/format/common/format'; +import {EditOperationsCommand} from 'vs/editor/contrib/format/common/formatCommand'; import {IConfigurationService} from 'vs/platform/configuration/common/configuration'; import {TextFileEditorModel} from 'vs/workbench/parts/files/common/editors/textFileEditorModel'; import {ExtHostContext, ExtHostDocumentSaveParticipantShape} from './extHost.protocol'; -// TODO@joh move this to the extension host -class TrimWhitespaceParticipant { - - private trimTrailingWhitespace: boolean = false; - private toUnbind: IDisposable[] = []; +class TrimWhitespaceParticipant implements ISaveParticipant { constructor( @IConfigurationService private configurationService: IConfigurationService, @ICodeEditorService private codeEditorService: ICodeEditorService ) { - this.registerListeners(); - this.onConfigurationChange(this.configurationService.getConfiguration()); - } - - public dispose(): void { - this.toUnbind = dispose(this.toUnbind); - } - - private registerListeners(): void { - this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationChange(e.config))); - } - - private onConfigurationChange(configuration: IFilesConfiguration): void { - this.trimTrailingWhitespace = configuration && configuration.files && configuration.files.trimTrailingWhitespace; + // Nothing } public participate(model: ITextFileEditorModel, env: { isAutoSaved: boolean }): any { - if (this.trimTrailingWhitespace) { + if (this.configurationService.lookup('files.trimTrailingWhitespace').value) { this.doTrimTrailingWhitespace(model.textEditorModel, env.isAutoSaved); } } @@ -89,34 +75,126 @@ class TrimWhitespaceParticipant { } } +class FormatOnSaveParticipant implements ISaveParticipant { + + constructor( + @ICodeEditorService private _editorService: ICodeEditorService, + @IConfigurationService private _configurationService: IConfigurationService + ) { + // Nothing + } + + participate(editorModel: ITextFileEditorModel, env: { isAutoSaved: boolean }): TPromise { + + if (!this._configurationService.lookup('files.formatOnSave').value) { + return; + } + + const model: IModel = editorModel.textEditorModel; + const editor = this._findEditor(model); + const {tabSize, insertSpaces} = model.getOptions(); + + return getDocumentRangeFormattingEdits(model, model.getFullModelRange(), { tabSize, insertSpaces }).then(edits => { + if (edits) { + if (editor) { + this._editsWithEditor(editor, edits, env.isAutoSaved); + } else { + this._editWithModel(model, edits); + } + } + }); + } + + private _editsWithEditor(editor: ICommonCodeEditor, edits: ISingleEditOperation[], isAutoSaved: boolean): void { + + if (isAutoSaved && editor.isFocused()) { + // when we save an focus (active) editor we check if + // formatting edits intersect with any cursor. iff so + // we ignore this + + let intersectsCursor = false; + outer: for (const selection of editor.getSelections()) { + for (const {range} of edits) { + if (Range.areIntersectingOrTouching(range, selection)) { + intersectsCursor = true; + break outer; + } + } + } + if (intersectsCursor) { + return; + } + } + + editor.executeCommand('files.formatOnSave', new EditOperationsCommand(edits, editor.getSelection())); + } + + private _editWithModel(model: IModel, edits: ISingleEditOperation[]): void { + model.applyEdits(edits.map(({text, range}) => ({ + text, + range: Range.lift(range), + identifier: undefined, + forceMoveMarkers: true + }))); + } + + private _findEditor(model: IModel) { + if (!model.isAttachedToEditor()) { + return; + } + + let candidate: ICommonCodeEditor; + for (const editor of this._editorService.listCodeEditors()) { + if (editor.getModel() === model) { + if (editor.isFocused()) { + return editor; + } else { + candidate = editor; + } + } + } + return candidate; + } +} + +class ExtHostSaveParticipant implements ISaveParticipant { + + private _proxy: ExtHostDocumentSaveParticipantShape; + + constructor( @IThreadService threadService: IThreadService) { + this._proxy = threadService.get(ExtHostContext.ExtHostDocumentSaveParticipant); + } + + participate(editorModel: ITextFileEditorModel, env: { isAutoSaved: boolean }): TPromise { + return this._proxy.$participateInSave(editorModel.getResource()); + } +} + // The save participant can change a model before its saved to support various scenarios like trimming trailing whitespace export class SaveParticipant implements ISaveParticipant { - private _mainThreadSaveParticipant: TrimWhitespaceParticipant; - private _extHostSaveParticipant: ExtHostDocumentSaveParticipantShape; + private _saveParticipants: ISaveParticipant[]; constructor( - @IConfigurationService configurationService: IConfigurationService, - @ICodeEditorService codeEditorService: ICodeEditorService, + @IInstantiationService instantiationService: IInstantiationService, @IThreadService threadService: IThreadService ) { - this._mainThreadSaveParticipant = new TrimWhitespaceParticipant(configurationService, codeEditorService); - this._extHostSaveParticipant = threadService.get(ExtHostContext.ExtHostDocumentSaveParticipant); + + this._saveParticipants = [ + instantiationService.createInstance(TrimWhitespaceParticipant), + instantiationService.createInstance(FormatOnSaveParticipant), + instantiationService.createInstance(ExtHostSaveParticipant) + ]; // Hook into model TextFileEditorModel.setSaveParticipant(this); } - - dispose() { - this._mainThreadSaveParticipant.dispose(); - } - participate(model: ITextFileEditorModel, env: { isAutoSaved: boolean }): TPromise { - try { - this._mainThreadSaveParticipant.participate(model, env); - } catch (err) { - // ignore - } - return this._extHostSaveParticipant.$participateInSave(model.getResource()); + const promiseFactory = this._saveParticipants.map(p => () => { + return TPromise.as(p.participate(model, env)).then(undefined, err => { + // console.error(err); + }); + }); + return sequence(promiseFactory); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/files/browser/files.contribution.ts b/src/vs/workbench/parts/files/browser/files.contribution.ts index 4029f5240e6..d9a1355dafe 100644 --- a/src/vs/workbench/parts/files/browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/browser/files.contribution.ts @@ -212,6 +212,11 @@ configurationRegistry.registerConfiguration({ 'default': false, 'description': nls.localize('trimTrailingWhitespace', "When enabled, will trim trailing whitespace when you save a file.") }, + 'files.formatOnSave': { + 'type': 'boolean', + 'default': false, + 'description': nls.localize('formatOnSave', "Format a file on save - a matching formatting provider must be available.") + }, 'files.autoSave': { 'type': 'string', 'enum': [AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, , AutoSaveConfiguration.ON_WINDOW_CHANGE], diff --git a/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts index 58bc8c2756d..991d586f1de 100644 --- a/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/test/node/api/extHostDocumentSaveParticipant.test.ts @@ -92,8 +92,7 @@ suite('ExtHostDocumentSaveParticipant', () => { sub.dispose(); const [first] = values; - assert.ok(first instanceof Error); - assert.ok((first).message); + assert.equal(first, false); }); }); @@ -134,6 +133,27 @@ suite('ExtHostDocumentSaveParticipant', () => { }); }); + test('event delivery, ignore bad listeners', () => { + const participant = new ExtHostDocumentSaveParticipant(documents, workspace, { timeout: 5, errors: 1 }); + + let callCount = 0; + let sub = participant.onWillSaveTextDocumentEvent(function (event) { + callCount += 1; + throw new Error('boom'); + }); + + return TPromise.join([ + participant.$participateInSave(resource), + participant.$participateInSave(resource), + participant.$participateInSave(resource), + participant.$participateInSave(resource) + + ]).then(values => { + sub.dispose(); + assert.equal(callCount, 2); + }); + }); + test('event delivery, waitUntil', () => { const participant = new ExtHostDocumentSaveParticipant(documents, workspace); @@ -174,7 +194,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, waitUntil will timeout', () => { - const participant = new ExtHostDocumentSaveParticipant(documents, workspace, 5); + const participant = new ExtHostDocumentSaveParticipant(documents, workspace, { timeout: 5, errors: 3 }); let sub = participant.onWillSaveTextDocumentEvent(function (event) { event.waitUntil(TPromise.timeout(15)); @@ -184,8 +204,7 @@ suite('ExtHostDocumentSaveParticipant', () => { sub.dispose(); const [first] = values; - assert.ok(first instanceof Error); - assert.ok((first).message); + assert.equal(first, false); }); }); @@ -256,7 +275,7 @@ suite('ExtHostDocumentSaveParticipant', () => { sub.dispose(); assert.equal(edits, undefined); - assert.ok((values[0]).message); + assert.equal(values[0], false); }); });