mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-24 18:49:00 +01:00
Merge branch 'master' into dbaeumer/TS2.0
This commit is contained in:
@@ -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<K, V> {
|
||||
// delete(key: K): boolean;
|
||||
get(key: K): V;
|
||||
// has(key: K): boolean;
|
||||
set(key: K, value?: V): WeakMap<K, V>;
|
||||
}
|
||||
|
||||
export class ExtHostDocumentSaveParticipant extends ExtHostDocumentSaveParticipantShape {
|
||||
|
||||
private _documents: ExtHostDocuments;
|
||||
private _workspace: MainThreadWorkspaceShape;
|
||||
private _listenerTimeout: number;
|
||||
private _callbacks = new CallbackList();
|
||||
private _badListeners = new WeakMap<Function, number>();
|
||||
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<vscode.TextDocumentWillSaveEvent> {
|
||||
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<any[]> {
|
||||
$participateInSave(resource: URI): TPromise<boolean[]> {
|
||||
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) || (<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<any[]>((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'));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<IFilesConfiguration>());
|
||||
}
|
||||
|
||||
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<any> {
|
||||
|
||||
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<any> {
|
||||
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<any> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user