diff --git a/src/editor/Editor.js b/src/editor/Editor.js index 0f6fc376b67..0e044e02cfa 100644 --- a/src/editor/Editor.js +++ b/src/editor/Editor.js @@ -905,6 +905,34 @@ define(function (require, exports, module) { return column; }; + /** + * Returns the string-based pos for a given display column (zero-based) in given line. Differs from column + * only when the line contains preceding \t chars. Result depends on the current tab size setting. + * @param {number} lineNum Line number + * @param {number} column Display column number + * @return {number} + */ + Editor.prototype.getCharIndexForColumn = function (lineNum, column) { + var line = this._codeMirror.getLine(lineNum), + tabSize = null, + iCol = 0, + i; + + for (i = 0; iCol < column; i++) { + if (line[i] === '\t') { + if (tabSize === null) { + tabSize = Editor.getTabSize(); + } + if (tabSize > 0) { + iCol += (tabSize - (iCol % tabSize)); + } + } else { + iCol++; + } + } + return i; + }; + /** * Sets the cursor position within the editor. Removes any selection. * @param {number} line The 0 based line number. diff --git a/src/editor/EditorCommandHandlers.js b/src/editor/EditorCommandHandlers.js index 6c9c84aca52..a20657584af 100644 --- a/src/editor/EditorCommandHandlers.js +++ b/src/editor/EditorCommandHandlers.js @@ -979,14 +979,22 @@ define(function (require, exports, module) { var origSels = editor.getSelections(), newSels = []; _.each(origSels, function (sel) { - var pos; + var pos, colOffset; if ((dir === -1 && sel.start.line > editor.getFirstVisibleLine()) || (dir === 1 && sel.end.line < editor.getLastVisibleLine())) { // Add a new cursor on the next line up/down. It's okay if it overlaps another selection, because CM // will take care of throwing it away in that case. It will also take care of clipping the char position // to the end of the new line if the line is shorter. pos = _.clone(dir === -1 ? sel.start : sel.end); + + // get sel column of current selection + colOffset = editor.getColOffset(pos); + pos.line += dir; + // translate column to ch in line of new selection + pos.ch = editor.getCharIndexForColumn(pos.line, colOffset); + + // If this is the primary selection, we want the new cursor we're adding to become the // primary selection. newSels.push({start: pos, end: pos, primary: sel.primary}); diff --git a/test/spec/EditorCommandHandlers-test.js b/test/spec/EditorCommandHandlers-test.js index 3c9d8174556..97c8f8487d2 100644 --- a/test/spec/EditorCommandHandlers-test.js +++ b/test/spec/EditorCommandHandlers-test.js @@ -47,6 +47,11 @@ define(function (require, exports, module) { "\n" + "}"; + var tabbedContent = "function funcWithTabs() {\n" + + " var i = 0;\n" + + " var offset = 0;\n" + + "}"; + var myDocument, myEditor; var testPath = SpecRunnerUtils.getTestPath("/spec/EditorCommandHandlers-test-files"), @@ -3316,6 +3321,26 @@ define(function (require, exports, module) { }); + describe("Add Line to Selection with Tabs", function () { + beforeEach(function () { + setupFullEditor(tabbedContent); + }); + + it("should add a cursor on the next line before a single cursor in same visual position", function () { + myEditor.setSelection({line: 1, ch: 8}, {line: 1, ch: 8}); + CommandManager.execute(Commands.EDIT_ADD_NEXT_LINE_TO_SEL, myEditor); + expectSelections([{start: {line: 1, ch: 8}, end: {line: 1, ch: 8}, primary: false, reversed: false}, + {start: {line: 2, ch: 12}, end: {line: 2, ch: 12}, primary: true, reversed: false}]); + }); + + it("should add a cursor on the previous line before a single cursor selection in same visual position", function () { + myEditor.setSelection({line: 2, ch: 12}, {line: 2, ch: 12}); + CommandManager.execute(Commands.EDIT_ADD_PREV_LINE_TO_SEL, myEditor); + expectSelections([{start: {line: 1, ch: 8}, end: {line: 1, ch: 8}, primary: true, reversed: false}, + {start: {line: 2, ch: 12}, end: {line: 2, ch: 12}, primary: false, reversed: false}]); + }); + }); + describe("EditorCommandHandlers Integration", function () { this.category = "integration";