diff --git a/src/vs/editor/contrib/indentation/common/indentation.ts b/src/vs/editor/contrib/indentation/common/indentation.ts index 7fb494c770d071f34458b388db323ddb2cf0e705..d9c270f2464ad518117491563845e0d7cda71dc4 100644 --- a/src/vs/editor/contrib/indentation/common/indentation.ts +++ b/src/vs/editor/contrib/indentation/common/indentation.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import * as strings from 'vs/base/common/strings'; -import { ICommonCodeEditor, IIdentifiedSingleEditOperation, ICommand, ICursorStateComputerData, IEditOperationBuilder, ITokenizedModel } from 'vs/editor/common/editorCommon'; +import { ICommonCodeEditor, IEditorContribution, IIdentifiedSingleEditOperation, ICommand, ICursorStateComputerData, IEditOperationBuilder, ITokenizedModel, EndOfLineSequence } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { editorAction, ServicesAccessor, IActionOptions, EditorAction } from 'vs/editor/common/editorCommonExtensions'; +import { editorAction, ServicesAccessor, IActionOptions, EditorAction, commonEditorContribution } from 'vs/editor/common/editorCommonExtensions'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IModelService } from 'vs/editor/common/services/modelService'; import { Range } from 'vs/editor/common/core/range'; @@ -17,6 +18,7 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { TextModel } from 'vs/editor/common/model/textModel'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand'; +import { TextEdit } from 'vs/editor/common/modes'; export function shiftIndent(tabSize: number, indentation: string, count?: number): string { count = count || 1; @@ -325,6 +327,233 @@ export class ReindentLinesAction extends EditorAction { } } +export class AutoIndentOnPasteCommand implements ICommand { + + private _edits: TextEdit[]; + private _newEol: EndOfLineSequence; + + private _initialSelection: Selection; + private _selectionId: string; + + constructor(edits: TextEdit[], initialSelection: Selection) { + this._initialSelection = initialSelection; + this._edits = []; + this._newEol = undefined; + + for (let edit of edits) { + if (typeof edit.eol === 'number') { + this._newEol = edit.eol; + } + if (edit.range && typeof edit.text === 'string') { + this._edits.push(edit); + } + } + } + + public getEditOperations(model: ITokenizedModel, builder: IEditOperationBuilder): void { + for (let edit of this._edits) { + builder.addEditOperation(Range.lift(edit.range), edit.text); + } + + var selectionIsSet = false; + if (Array.isArray(this._edits) && this._edits.length === 1 && this._initialSelection.isEmpty()) { + if (this._edits[0].range.startColumn === this._initialSelection.endColumn && + this._edits[0].range.startLineNumber === this._initialSelection.endLineNumber) { + selectionIsSet = true; + this._selectionId = builder.trackSelection(this._initialSelection, true); + } else if (this._edits[0].range.endColumn === this._initialSelection.startColumn && + this._edits[0].range.endLineNumber === this._initialSelection.startLineNumber) { + selectionIsSet = true; + this._selectionId = builder.trackSelection(this._initialSelection, false); + } + } + + if (!selectionIsSet) { + this._selectionId = builder.trackSelection(this._initialSelection); + } + } + + public computeCursorState(model: ITokenizedModel, helper: ICursorStateComputerData): Selection { + return helper.getTrackedSelection(this._selectionId); + } +} + +@commonEditorContribution +export class AutoIndentOnPaste implements IEditorContribution { + private static ID = 'editor.contrib.autoIndentOnPaste'; + + private editor: ICommonCodeEditor; + private callOnDispose: IDisposable[]; + private callOnModel: IDisposable[]; + + constructor(editor: ICommonCodeEditor) { + this.editor = editor; + 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())); + } + + private update(): void { + + // clean up + this.callOnModel = dispose(this.callOnModel); + + // we are disabled + if (!this.editor.getConfiguration().autoIndent) { + return; + } + + // no model + if (!this.editor.getModel()) { + 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(); + this.editor.pushUndoStop(); + let textEdits: TextEdit[] = []; + + let indentConverter = { + shiftIndent: (indentation) => { + let desiredIndentCount = ShiftCommand.shiftIndentCount(indentation, indentation.length + 1, tabSize); + let newIndentation = ''; + for (let i = 0; i < desiredIndentCount; i++) { + newIndentation += '\t'; + } + + return newIndentation; + }, + unshiftIndent: (indentation) => { + let desiredIndentCount = ShiftCommand.unshiftIndentCount(indentation, indentation.length + 1, tabSize); + let newIndentation = ''; + for (let i = 0; i < desiredIndentCount; i++) { + newIndentation += '\t'; + } + + return newIndentation; + } + }; + let indentOfFirstLine = LanguageConfigurationRegistry.getGoodIndentForLine(model, model.getLanguageIdentifier().id, range.startLineNumber, indentConverter); + + if (indentOfFirstLine !== null) { + let firstLineText = model.getLineContent(range.startLineNumber); + let oldIndentation = strings.getLeadingWhitespace(firstLineText); + let newSpaceCnt = this.getSpaceCnt(indentOfFirstLine, tabSize); + let oldSpaceCnt = this.getSpaceCnt(oldIndentation, tabSize); + + if (newSpaceCnt !== oldSpaceCnt) { + let newIndent = this.generateIndent(newSpaceCnt, tabSize, insertSpaces); + textEdits.push({ + range: new Range(range.startLineNumber, 1, range.startLineNumber, oldIndentation.length + 1), + text: newIndent + }); + firstLineText = newIndent + firstLineText.substr(oldIndentation.length + 1); + } + + if (range.startLineNumber !== range.endLineNumber) { + let virtualModel = { + getLineTokens: (lineNumber: number) => { + return model.getLineTokens(lineNumber); + }, + getLanguageIdentifier: () => { + return model.getLanguageIdentifier(); + }, + getLanguageIdAtPosition: (lineNumber: number, column: number) => { + return model.getLanguageIdAtPosition(lineNumber, column); + }, + getLineContent: (lineNumber) => { + if (lineNumber === range.startLineNumber) { + return firstLineText; + } else { + return model.getLineContent(lineNumber); + } + } + }; + let indentOfSecondLine = LanguageConfigurationRegistry.getGoodIndentForLine(virtualModel, model.getLanguageIdentifier().id, range.startLineNumber + 1, indentConverter); + let newSpaceCntOfSecondLine = this.getSpaceCnt(indentOfSecondLine, tabSize); + let oldSpaceCntOfSecondLine = this.getSpaceCnt(strings.getLeadingWhitespace(model.getLineContent(range.startLineNumber + 1)), tabSize); + + if (newSpaceCntOfSecondLine !== oldSpaceCntOfSecondLine) { + let spaceCntOffset = newSpaceCntOfSecondLine - oldSpaceCntOfSecondLine; + for (let i = range.startLineNumber + 1; i <= range.endLineNumber; i++) { + let lineContent = model.getLineContent(i); + let originalIndent = strings.getLeadingWhitespace(lineContent); + let originalSpacesCnt = this.getSpaceCnt(originalIndent, tabSize); + let newSpacesCnt = originalSpacesCnt + spaceCntOffset; + let newIndent = this.generateIndent(newSpacesCnt, tabSize, insertSpaces); + + if (newIndent !== originalIndent) { + textEdits.push({ + range: new Range(i, 1, i, originalIndent.length), + text: newIndent + }); + } + } + } + } + } + + let cmd = new AutoIndentOnPasteCommand(textEdits, this.editor.getSelection()); + this.editor.executeCommand('autoIndentOnPaste', cmd); + this.editor.pushUndoStop(); + } + + public getId(): string { + return AutoIndentOnPaste.ID; + } + + public dispose(): void { + this.callOnDispose = dispose(this.callOnDispose); + this.callOnModel = dispose(this.callOnModel); + } + + private getSpaceCnt(str, tabSize) { + let spacesCnt = 0; + + for (let i = 0; i < str.length; i++) { + if (str.charAt(i) === '\t') { + spacesCnt += tabSize; + } else { + spacesCnt++; + } + } + + return spacesCnt; + } + + private generateIndent(spacesCnt: number, tabSize, insertSpaces) { + spacesCnt = spacesCnt < 0 ? 0 : spacesCnt; + + let result = ''; + if (!insertSpaces) { + let tabsCnt = Math.floor(spacesCnt / tabSize); + spacesCnt = spacesCnt % tabSize; + for (let i = 0; i < tabsCnt; i++) { + result += '\t'; + } + } + + for (let i = 0; i < spacesCnt; i++) { + result += ' '; + } + + return result; + } +} + function getIndentationEditOperations(model: ITokenizedModel, builder: IEditOperationBuilder, tabSize: number, tabsToSpaces: boolean): void { if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) { // Model is empty