diff --git a/src/vs/editor/common/commonCodeEditor.ts b/src/vs/editor/common/commonCodeEditor.ts index 3bdb7b417d209..1ef8b977c6952 100644 --- a/src/vs/editor/common/commonCodeEditor.ts +++ b/src/vs/editor/common/commonCodeEditor.ts @@ -53,6 +53,7 @@ export abstract class CommonCodeEditor extends EventEmitter implements editorCom public readonly onDidDispose: Event = fromEventEmitter(this, editorCommon.EventType.Disposed); public readonly onWillType: Event = fromEventEmitter(this, editorCommon.EventType.WillType); public readonly onDidType: Event = fromEventEmitter(this, editorCommon.EventType.DidType); + public readonly onDidPaste: Event = fromEventEmitter(this, editorCommon.EventType.DidPaste); protected domElement: IContextKeyServiceTarget; @@ -588,6 +589,20 @@ export abstract class CommonCodeEditor extends EventEmitter implements editorCom return; } + if (handlerId === editorCommon.Handler.Paste) { + if (!this.cursor || typeof payload.text !== 'string' || payload.text.length === 0) { + // nothing to do + return; + } + const startPosition = this.cursor.getSelection().getStartPosition(); + this.cursor.trigger(source, handlerId, payload); + const endPosition = this.cursor.getSelection().getStartPosition(); + if (source === 'keyboard') { + this.emit(editorCommon.EventType.DidPaste, new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column)); + } + return; + } + let candidate = this.getAction(handlerId); if (candidate !== null) { TPromise.as(candidate.run()).done(null, onUnexpectedError); diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index a02ce33f2bfef..5e0c5303700f9 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -287,6 +287,7 @@ class InternalEditorOptionsHelper { parameterHints: toBoolean(opts.parameterHints), iconsInSuggestions: toBoolean(opts.iconsInSuggestions), formatOnType: toBoolean(opts.formatOnType), + formatOnPaste: toBoolean(opts.formatOnPaste), suggestOnTriggerCharacters: toBoolean(opts.suggestOnTriggerCharacters), acceptSuggestionOnEnter: toBoolean(opts.acceptSuggestionOnEnter), snippetSuggestions: opts.snippetSuggestions, @@ -666,6 +667,11 @@ const editorConfiguration: IConfigurationNode = { 'default': DefaultConfig.editor.formatOnType, 'description': nls.localize('formatOnType', "Controls if the editor should automatically format the line after typing") }, + 'editor.formatOnPaste': { + 'type': 'boolean', + 'default': DefaultConfig.editor.formatOnPaste, + 'description': nls.localize('formatOnPaste', "Controls if the editor should automatically format the pasted content") + }, 'editor.suggestOnTriggerCharacters': { 'type': 'boolean', 'default': DefaultConfig.editor.suggestOnTriggerCharacters, diff --git a/src/vs/editor/common/config/defaultConfig.ts b/src/vs/editor/common/config/defaultConfig.ts index ea15b26c2d1f9..86eb39b92ad75 100644 --- a/src/vs/editor/common/config/defaultConfig.ts +++ b/src/vs/editor/common/config/defaultConfig.ts @@ -83,6 +83,7 @@ class ConfigClass implements IConfiguration { iconsInSuggestions: true, autoClosingBrackets: true, formatOnType: false, + formatOnPaste: false, suggestOnTriggerCharacters: true, acceptSuggestionOnEnter: true, snippetSuggestions: 'bottom', diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 5a95b9a7335e5..8f2aede0d547a 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -393,6 +393,11 @@ export interface IEditorOptions { * Defaults to false. */ formatOnType?: boolean; + /** + * Enable format on paste. + * Defaults to false. + */ + formatOnPaste?: boolean; /** * Enable the suggestion box to pop-up on trigger characters. * Defaults to true. @@ -879,6 +884,7 @@ export class EditorContribOptions { readonly parameterHints: boolean; readonly iconsInSuggestions: boolean; readonly formatOnType: boolean; + readonly formatOnPaste: boolean; readonly suggestOnTriggerCharacters: boolean; readonly acceptSuggestionOnEnter: boolean; readonly snippetSuggestions: 'top' | 'bottom' | 'inline' | 'none'; @@ -903,6 +909,7 @@ export class EditorContribOptions { parameterHints: boolean; iconsInSuggestions: boolean; formatOnType: boolean; + formatOnPaste: boolean; suggestOnTriggerCharacters: boolean; acceptSuggestionOnEnter: boolean; snippetSuggestions: 'top' | 'bottom' | 'inline' | 'none'; @@ -923,6 +930,7 @@ export class EditorContribOptions { this.parameterHints = Boolean(source.parameterHints); this.iconsInSuggestions = Boolean(source.iconsInSuggestions); this.formatOnType = Boolean(source.formatOnType); + this.formatOnPaste = Boolean(source.formatOnPaste); this.suggestOnTriggerCharacters = Boolean(source.suggestOnTriggerCharacters); this.acceptSuggestionOnEnter = Boolean(source.acceptSuggestionOnEnter); this.snippetSuggestions = source.snippetSuggestions; @@ -949,6 +957,7 @@ export class EditorContribOptions { && this.parameterHints === other.parameterHints && this.iconsInSuggestions === other.iconsInSuggestions && this.formatOnType === other.formatOnType + && this.formatOnPaste === other.formatOnPaste && this.suggestOnTriggerCharacters === other.suggestOnTriggerCharacters && this.acceptSuggestionOnEnter === other.acceptSuggestionOnEnter && this.snippetSuggestions === other.snippetSuggestions @@ -3800,6 +3809,13 @@ export interface ICommonCodeEditor extends IEditor { */ onDidType(listener: (text: string) => void): IDisposable; + /** + * An event emitted when users paste text in the editor. + * @event + * @internal + */ + onDidPaste(listener: (range: Range) => void): IDisposable; + /** * Returns true if this editor or one of its widgets has keyboard focus. */ @@ -4098,6 +4114,8 @@ export var EventType = { WillType: 'willType', DidType: 'didType', + DidPaste: 'didPaste', + EditorLayout: 'editorLayout', DiffUpdated: 'diffUpdated' diff --git a/src/vs/editor/contrib/format/common/formatActions.ts b/src/vs/editor/contrib/format/common/formatActions.ts index 73737d382883f..2b6e35c84dcd3 100644 --- a/src/vs/editor/contrib/format/common/formatActions.ts +++ b/src/vs/editor/contrib/format/common/formatActions.ts @@ -12,13 +12,14 @@ import { TPromise } from 'vs/base/common/winjs.base'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { editorAction, ServicesAccessor, EditorAction, commonEditorContribution } from 'vs/editor/common/editorCommonExtensions'; -import { OnTypeFormattingEditProviderRegistry } from 'vs/editor/common/modes'; +import { OnTypeFormattingEditProviderRegistry, DocumentRangeFormattingEditProviderRegistry } from 'vs/editor/common/modes'; import { getOnTypeFormattingEdits, getDocumentFormattingEdits, getDocumentRangeFormattingEdits } from '../common/format'; import { EditOperationsCommand } from './formatCommand'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { CharacterSet } from 'vs/editor/common/core/characterClassifier'; +import { Range } from 'vs/editor/common/core/range'; import ModeContextKeys = editorCommon.ModeContextKeys; import EditorContextKeys = editorCommon.EditorContextKeys; @@ -150,6 +151,86 @@ class FormatOnType implements editorCommon.IEditorContribution { } } +@commonEditorContribution +class FormatOnPaste implements editorCommon.IEditorContribution { + + private static ID = 'editor.contrib.formatOnPaste'; + + private editor: editorCommon.ICommonCodeEditor; + private workerService: IEditorWorkerService; + private callOnDispose: IDisposable[]; + private callOnModel: IDisposable[]; + + constructor(editor: editorCommon.ICommonCodeEditor, @IEditorWorkerService workerService: IEditorWorkerService) { + this.editor = editor; + this.workerService = workerService; + this.callOnDispose = []; + this.callOnModel = []; + + this.callOnDispose.push(editor.onDidChangeConfiguration(() => this.update())); + this.callOnDispose.push(editor.onDidChangeModel(() => this.update())); + this.callOnDispose.push(editor.onDidChangeModelLanguage(() => this.update())); + this.callOnDispose.push(DocumentRangeFormattingEditProviderRegistry.onDidChange(this.update, this)); + } + + private update(): void { + + // clean up + this.callOnModel = dispose(this.callOnModel); + + // we are disabled + if (!this.editor.getConfiguration().contribInfo.formatOnPaste) { + return; + } + + // no model + if (!this.editor.getModel()) { + return; + } + + let model = this.editor.getModel(); + + // no support + let [support] = DocumentRangeFormattingEditProviderRegistry.ordered(model); + if (!support || !support.provideDocumentRangeFormattingEdits) { + return; + } + + this.callOnModel.push(this.editor.onDidPaste((range: Range) => { + this.trigger(range); + })); + } + + private trigger(range: Range): void { + if (this.editor.getSelections().length > 1) { + return; + } + + const model = this.editor.getModel(); + const { tabSize, insertSpaces } = model.getOptions(); + const state = this.editor.captureState(editorCommon.CodeEditorStateFlag.Value, editorCommon.CodeEditorStateFlag.Position); + + getDocumentRangeFormattingEdits(model, range, { tabSize, insertSpaces }).then(edits => { + return this.workerService.computeMoreMinimalEdits(model.uri, edits, []); + }).then(edits => { + if (!state.validate(this.editor) || isFalsyOrEmpty(edits)) { + return; + } + const command = new EditOperationsCommand(edits, this.editor.getSelection()); + this.editor.executeCommand(this.getId(), command); + }); + } + + public getId(): string { + return FormatOnPaste.ID; + } + + public dispose(): void { + this.callOnDispose = dispose(this.callOnDispose); + this.callOnModel = dispose(this.callOnModel); + } +} + export abstract class AbstractFormatAction extends EditorAction { public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): TPromise { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 3dad2cce5835f..a79e649b22657 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1303,6 +1303,11 @@ declare module monaco.editor { * Defaults to false. */ formatOnType?: boolean; + /** + * Enable format on paste. + * Defaults to false. + */ + formatOnPaste?: boolean; /** * Enable the suggestion box to pop-up on trigger characters. * Defaults to true. @@ -1521,6 +1526,7 @@ declare module monaco.editor { readonly parameterHints: boolean; readonly iconsInSuggestions: boolean; readonly formatOnType: boolean; + readonly formatOnPaste: boolean; readonly suggestOnTriggerCharacters: boolean; readonly acceptSuggestionOnEnter: boolean; readonly snippetSuggestions: 'top' | 'bottom' | 'inline' | 'none'; diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index fbc9f69d62007..2fcc38f81f4a7 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -169,6 +169,7 @@ const configurationValueWhitelist = [ 'editor.detectIndentation', 'editor.formatOnType', 'editor.formatOnSave', + 'editor.formatOnPaste', 'window.openFilesInNewWindow', 'javascript.validate.enable', 'editor.mouseWheelZoom',