From a524360ef2705a91c9af6fe380cd98c8a044445e Mon Sep 17 00:00:00 2001 From: rebornix Date: Thu, 12 Jan 2017 15:36:07 -0800 Subject: [PATCH 1/4] fix #13945. support format on paste --- .../contrib/format/common/formatActions.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/vs/editor/contrib/format/common/formatActions.ts b/src/vs/editor/contrib/format/common/formatActions.ts index 73737d382883f..646e91a35bac5 100644 --- a/src/vs/editor/contrib/format/common/formatActions.ts +++ b/src/vs/editor/contrib/format/common/formatActions.ts @@ -19,6 +19,7 @@ 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; @@ -235,6 +236,33 @@ export class FormatSelectionAction extends AbstractFormatAction { } } +@editorAction +export class FormatOnPasteAction extends AbstractFormatAction { + constructor() { + super({ + id: 'editor.action.formatOnPaste', + label: nls.localize('formatOnPaste.label', "Format on paste"), + alias: 'Format on paste', + precondition: EditorContextKeys.Writable + }); + } + + protected _getFormattingEdits(editor: editorCommon.ICommonCodeEditor): TPromise { + const originalSelectionStart = editor.getSelection().getStartPosition(); + editor.focus(); + document.execCommand('paste'); + + // paste doesn't persist selection + const currentCursorPosition = editor.getSelection().getStartPosition(); + const pastedContentRange = new Range(currentCursorPosition.lineNumber, currentCursorPosition.column, originalSelectionStart.lineNumber, originalSelectionStart.column); + + const model = editor.getModel(); + const { tabSize, insertSpaces} = model.getOptions(); + + return getDocumentRangeFormattingEdits(model, pastedContentRange, { tabSize, insertSpaces }); + } +} + // this is the old format action that does both (format document OR format selection) // and we keep it here such that existing keybinding configurations etc will still work CommandsRegistry.registerCommand('editor.action.format', accessor => { From 3f3ea06fa3bacb1878b548696db482c736e26143 Mon Sep 17 00:00:00 2001 From: rebornix Date: Fri, 13 Jan 2017 10:48:13 -0800 Subject: [PATCH 2/4] update key bindings for format on paste --- .../contrib/format/common/formatActions.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/vs/editor/contrib/format/common/formatActions.ts b/src/vs/editor/contrib/format/common/formatActions.ts index 646e91a35bac5..2b8bfc7c6ea82 100644 --- a/src/vs/editor/contrib/format/common/formatActions.ts +++ b/src/vs/editor/contrib/format/common/formatActions.ts @@ -237,13 +237,21 @@ export class FormatSelectionAction extends AbstractFormatAction { } @editorAction -export class FormatOnPasteAction extends AbstractFormatAction { +export class PasteAndFormatAction extends AbstractFormatAction { constructor() { super({ - id: 'editor.action.formatOnPaste', - label: nls.localize('formatOnPaste.label', "Format on paste"), - alias: 'Format on paste', - precondition: EditorContextKeys.Writable + id: 'editor.action.pasteAndFormat', + label: nls.localize('pasteAndFormat.label', "Paste and Format"), + alias: 'Paste and Format', + precondition: ContextKeyExpr.and(EditorContextKeys.Writable, ModeContextKeys.hasDocumentSelectionFormattingProvider), + kbOpts: { + kbExpr: EditorContextKeys.TextFocus, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_V) + }, + menuOpts: { + group: '1_modification', + order: 1.32 + } }); } From b9241a732f8422e6fc3bfd774892d049ff317ec9 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 17 Jan 2017 12:45:46 -0800 Subject: [PATCH 3/4] formatOnPaste as editor option instead of actions --- src/vs/editor/common/commonCodeEditor.ts | 20 +++ .../common/config/commonEditorConfig.ts | 6 + src/vs/editor/common/config/defaultConfig.ts | 1 + src/vs/editor/common/editorCommon.ts | 18 +++ .../contrib/format/common/formatActions.ts | 118 ++++++++++++------ src/vs/monaco.d.ts | 6 + .../telemetry/common/telemetryUtils.ts | 1 + 7 files changed, 134 insertions(+), 36 deletions(-) diff --git a/src/vs/editor/common/commonCodeEditor.ts b/src/vs/editor/common/commonCodeEditor.ts index 3bdb7b417d209..89f99ec4b1b42 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,25 @@ 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, { + startLineNumber: startPosition.lineNumber, + startColumn: startPosition.column, + endLineNumber: endPosition.lineNumber, + endColumn: 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 2b8bfc7c6ea82..72bf2749aab9f 100644 --- a/src/vs/editor/contrib/format/common/formatActions.ts +++ b/src/vs/editor/contrib/format/common/formatActions.ts @@ -12,7 +12,7 @@ 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'; @@ -151,6 +151,87 @@ 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; + } + + var model = this.editor.getModel(); + + // no support + var [support] = OnTypeFormattingEditProviderRegistry.ordered(model); + if (!support || !support.autoFormatTriggerCharacters) { + 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); + this.editor.focus(); + }); + } + + 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 { @@ -236,41 +317,6 @@ export class FormatSelectionAction extends AbstractFormatAction { } } -@editorAction -export class PasteAndFormatAction extends AbstractFormatAction { - constructor() { - super({ - id: 'editor.action.pasteAndFormat', - label: nls.localize('pasteAndFormat.label', "Paste and Format"), - alias: 'Paste and Format', - precondition: ContextKeyExpr.and(EditorContextKeys.Writable, ModeContextKeys.hasDocumentSelectionFormattingProvider), - kbOpts: { - kbExpr: EditorContextKeys.TextFocus, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_V) - }, - menuOpts: { - group: '1_modification', - order: 1.32 - } - }); - } - - protected _getFormattingEdits(editor: editorCommon.ICommonCodeEditor): TPromise { - const originalSelectionStart = editor.getSelection().getStartPosition(); - editor.focus(); - document.execCommand('paste'); - - // paste doesn't persist selection - const currentCursorPosition = editor.getSelection().getStartPosition(); - const pastedContentRange = new Range(currentCursorPosition.lineNumber, currentCursorPosition.column, originalSelectionStart.lineNumber, originalSelectionStart.column); - - const model = editor.getModel(); - const { tabSize, insertSpaces} = model.getOptions(); - - return getDocumentRangeFormattingEdits(model, pastedContentRange, { tabSize, insertSpaces }); - } -} - // this is the old format action that does both (format document OR format selection) // and we keep it here such that existing keybinding configurations etc will still work CommandsRegistry.registerCommand('editor.action.format', accessor => { 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', From 487c17266bdb123b97756b025536e74ab09b5574 Mon Sep 17 00:00:00 2001 From: rebornix Date: Thu, 19 Jan 2017 07:53:10 -0800 Subject: [PATCH 4/4] Address comments. --- src/vs/editor/common/commonCodeEditor.ts | 7 +------ src/vs/editor/contrib/format/common/formatActions.ts | 7 +++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/vs/editor/common/commonCodeEditor.ts b/src/vs/editor/common/commonCodeEditor.ts index 89f99ec4b1b42..1ef8b977c6952 100644 --- a/src/vs/editor/common/commonCodeEditor.ts +++ b/src/vs/editor/common/commonCodeEditor.ts @@ -598,12 +598,7 @@ export abstract class CommonCodeEditor extends EventEmitter implements editorCom this.cursor.trigger(source, handlerId, payload); const endPosition = this.cursor.getSelection().getStartPosition(); if (source === 'keyboard') { - this.emit(editorCommon.EventType.DidPaste, { - startLineNumber: startPosition.lineNumber, - startColumn: startPosition.column, - endLineNumber: endPosition.lineNumber, - endColumn: endPosition.column - }); + this.emit(editorCommon.EventType.DidPaste, new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column)); } return; } diff --git a/src/vs/editor/contrib/format/common/formatActions.ts b/src/vs/editor/contrib/format/common/formatActions.ts index 72bf2749aab9f..2b6e35c84dcd3 100644 --- a/src/vs/editor/contrib/format/common/formatActions.ts +++ b/src/vs/editor/contrib/format/common/formatActions.ts @@ -188,11 +188,11 @@ class FormatOnPaste implements editorCommon.IEditorContribution { return; } - var model = this.editor.getModel(); + let model = this.editor.getModel(); // no support - var [support] = OnTypeFormattingEditProviderRegistry.ordered(model); - if (!support || !support.autoFormatTriggerCharacters) { + let [support] = DocumentRangeFormattingEditProviderRegistry.ordered(model); + if (!support || !support.provideDocumentRangeFormattingEdits) { return; } @@ -218,7 +218,6 @@ class FormatOnPaste implements editorCommon.IEditorContribution { } const command = new EditOperationsCommand(edits, this.editor.getSelection()); this.editor.executeCommand(this.getId(), command); - this.editor.focus(); }); }