diff --git a/lib/ace/commands/default_commands.js b/lib/ace/commands/default_commands.js index 807758d8..28ce390f 100644 --- a/lib/ace/commands/default_commands.js +++ b/lib/ace/commands/default_commands.js @@ -118,13 +118,15 @@ exports.commands = [{ bindKey: bindKey("Ctrl-Shift-Home", "Command-Shift-Up"), exec: function(editor) { editor.getSelection().selectFileStart(); }, multiSelectAction: "forEach", - readOnly: true + readOnly: true, + group: "fileJump" }, { name: "gotostart", bindKey: bindKey("Ctrl-Home", "Command-Home|Command-Up"), exec: function(editor) { editor.navigateFileStart(); }, multiSelectAction: "forEach", - readOnly: true + readOnly: true, + group: "fileJump" }, { name: "selectup", bindKey: bindKey("Shift-Up", "Shift-Up"), @@ -142,13 +144,15 @@ exports.commands = [{ bindKey: bindKey("Ctrl-Shift-End", "Command-Shift-Down"), exec: function(editor) { editor.getSelection().selectFileEnd(); }, multiSelectAction: "forEach", - readOnly: true + readOnly: true, + group: "fileJump" }, { name: "gotoend", bindKey: bindKey("Ctrl-End", "Command-End|Command-Down"), exec: function(editor) { editor.navigateFileEnd(); }, multiSelectAction: "forEach", - readOnly: true + readOnly: true, + group: "fileJump" }, { name: "selectdown", bindKey: bindKey("Shift-Down", "Shift-Down"), diff --git a/lib/ace/edit_session.js b/lib/ace/edit_session.js index 369a2bc0..17252d29 100644 --- a/lib/ace/edit_session.js +++ b/lib/ace/edit_session.js @@ -406,10 +406,11 @@ var EditSession = function(text, mode) { if (self.$deltas.length > 0) { undoManager.execute({ action: "aceupdate", - args: [self.$deltas, self] + args: [self.$deltas, self], + merge: self.mergeUndoDeltas }); } - + self.mergeUndoDeltas = false; self.$deltas = []; } this.$informUndoManager = lang.delayedCall(this.$syncInformUndoManager); @@ -1220,7 +1221,7 @@ var EditSession = function(text, mode) { var range, point; var lastDeltaIsInsert = false; if (isInsert(delta)) { - range = delta.range.clone(); + range = Range.fromPoints(delta.range.start, delta.range.end); lastDeltaIsInsert = true; } else { range = Range.fromPoints(delta.range.start, delta.range.start); @@ -1252,6 +1253,11 @@ var EditSession = function(text, mode) { // Check if this range and the last undo range has something in common. // If true, merge the ranges. if (lastUndoRange != null) { + if (Range.comparePoints(lastUndoRange.start, range.start) == 0) { + lastUndoRange.start.column += range.end.column - range.start.column; + lastUndoRange.end.column += range.end.column - range.start.column; + } + var cmp = lastUndoRange.compareRange(range); if (cmp == 1) { range.setStart(lastUndoRange.start); diff --git a/lib/ace/editor.js b/lib/ace/editor.js index b279ef7f..69d8f126 100644 --- a/lib/ace/editor.js +++ b/lib/ace/editor.js @@ -50,8 +50,6 @@ var defaultCommands = require("./commands/default_commands").commands; var config = require("./config"); /** - * - * * The main entry point into the Ace functionality. * * The `Editor` manages the [[EditSession]] (which manages [[Document]]s), as well as the [[VirtualRenderer]], which draws everything to the screen. @@ -88,6 +86,11 @@ var Editor = function(renderer, session) { wrap: true }); + this.$historyTracker = this.$historyTracker.bind(this); + this.commands.on("exec", this.$historyTracker); + + this.$initOperationListeners(); + this.setSession(session || new EditSession("")); config.resetOptions(this); config._emit("editor", this); @@ -97,6 +100,115 @@ var Editor = function(renderer, session) { oop.implement(this, EventEmitter); + this.$initOperationListeners = function() { + function last(a) {return a[a.length - 1]}; + + this.selections = []; + this.commands.on("exec", function(e) { + this.startOperation(e); + + var command = e.command; + if (command.group == "fileJump") { + var prev = this.prevOp; + if (!prev || prev.command.group != "fileJump") { + this.lastFileJumpPos = last(this.selections) + } + } else { + this.lastFileJumpPos = null; + } + }.bind(this), true); + + this.commands.on("afterExec", function(e) { + var command = e.command; + + if (command.group == "fileJump") { + if (this.lastFileJumpPos && !this.curOp.selectionChanged) { + this.selection.fromJSON(this.lastFileJumpPos); + return + } + } + this.endOperation(e); + }.bind(this), true); + + this.$opResetTimer = lang.delayedCall(this.endOperation.bind(this)); + + this.on("change", function() { + this.curOp || this.startOperation(); + this.curOp.docChanged = true; + }.bind(this), true); + + this.on("changeSelection", function() { + this.curOp || this.startOperation(); + this.curOp.selectionChanged = true; + }.bind(this), true); + } + + this.curOp = null; + this.prevOp = {}; + this.startOperation = function(commadEvent) { + if (this.curOp) { + if (!commadEvent || this.curOp.command) + return; + this.prevOp = this.curOp; + } + if (!commadEvent) { + this.previousCommand = null; + commadEvent = {}; + } + + this.$opResetTimer.schedule(); + this.curOp = { + command: commadEvent.command || {}, + args: commadEvent.args + }; + + this.selections.push(this.selection.toJSON()); + }; + + this.endOperation = function() { + if (this.curOp) { + this.prevOp = this.curOp; + this.curOp = null; + } + }; + + this.$historyTracker = function(e) { + if (!this.$mergeUndoDeltas) + return; + + + var prev = this.prevOp; + var mergeableCommands = ["backspace", "del", "insertstring"]; + // previous command was the same + var shouldMerge = prev.command && (e.command.name == prev.command.name); + if (e.command.name == "insertstring") { + var text = e.args; + if (this.mergeNextCommand === undefined) + this.mergeNextCommand = true; + + shouldMerge = shouldMerge + && this.mergeNextCommand // previous command allows to coalesce with + && (!/\s/.test(text) || /\s/.test(prev.args)) // previous insertion was of same type + + this.mergeNextCommand = true; + } else { + shouldMerge = shouldMerge + && mergeableCommands.indexOf(e.command.name) !== -1// the command is mergeable + } + + if ( + this.$mergeUndoDeltas != "always" + && Date.now() - this.sequenceStartTime > 2000 + ) { + shouldMerge = false; // the sequence is too long + } + + if (shouldMerge) + this.session.mergeUndoDeltas = true; + else if (mergeableCommands.indexOf(e.command.name) !== -1) + this.sequenceStartTime = Date.now(); + }; + /** * Sets a new key handler, such as "vim" or "windows". * @param {String} keyboardHandler The new key handler @@ -701,8 +813,14 @@ var Editor = function(renderer, session) { if (this.getBehavioursEnabled()) { // Get a transform if the current mode wants one. var transform = mode.transformAction(session.getState(cursor.row), 'insertion', this, session, text); - if (transform) + if (transform) { + if (text !== transform.text) { + this.session.mergeUndoDeltas = false; + this.$mergeNextCommand = false; + } text = transform.text; + + } } if (text == "\t") @@ -2242,6 +2360,10 @@ config.defineOptions(Editor.prototype, "editor", { values: ["ace", "slim", "smooth", "wide"], initialValue: "ace" }, + mergeUndoDeltas: { + values: [false, true, "always"], + initialValue: true + }, behavioursEnabled: {initialValue: true}, wrapBehavioursEnabled: {initialValue: true}, diff --git a/lib/ace/multi_select_test.js b/lib/ace/multi_select_test.js index b1c9f397..45ab4965 100644 --- a/lib/ace/multi_select_test.js +++ b/lib/ace/multi_select_test.js @@ -171,6 +171,30 @@ module.exports = { selection.addRange(range2); editor.execCommand('insertfoo'); assert.equal('l1foo\nl2foo', editor.getValue()); + }, + + "test multiselect fromJSON/toJSON": function() { + var doc = new EditSession(["l1", "l2"]); + editor = new Editor(new MockRenderer(), doc); + MultiSelect(editor); + var selection = editor.selection; + + var before = selection.toJSON(); + + var range1 = new Range(0,2,0,2); + var range2 = new Range(1,2,1,2); + selection.fromOrientedRange(range1) + selection.addRange(range2); + + var after = selection.toJSON(); + + selection.fromJSON(before); + assert.ok(!selection.isEqual(after)); + assert.ok(selection.isEqual(before)); + + selection.fromJSON(after); + assert.ok(!selection.isEqual(before)); + assert.ok(selection.isEqual(after)); } }; diff --git a/lib/ace/selection.js b/lib/ace/selection.js index 4208fe01..ad1163a4 100644 --- a/lib/ace/selection.js +++ b/lib/ace/selection.js @@ -3,7 +3,7 @@ * * Copyright (c) 2010, Ajax.org B.V. * All rights reserved. - * + * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright @@ -14,7 +14,7 @@ * * Neither the name of Ajax.org B.V. nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -37,7 +37,7 @@ var EventEmitter = require("./lib/event_emitter").EventEmitter; var Range = require("./range").Range; /** - * + * * Contains the cursor position and the text selection of an edit session. * * The row/columns used in the selection are in document coordinates representing ths coordinates as thez appear in the document before applying soft wrap and folding. @@ -49,22 +49,22 @@ var Range = require("./range").Range; * Emitted when the cursor position changes. * @event changeCursor * - * + * * **/ /** * Emitted when the cursor selection changes. * @event changeSelection * - * + * * **/ /** * Creates a new `Selection` object. * @param {EditSession} session The session to use * - * - * + * + * * @constructor **/ var Selection = function(session) { @@ -95,7 +95,7 @@ var Selection = function(session) { oop.implement(this, EventEmitter); /** - * + * * Returns `true` if the selection is empty. * @returns {Boolean} **/ @@ -106,7 +106,7 @@ var Selection = function(session) { )); }; - /** + /** * Returns `true` if the selection is a multi-line. * @returns {Boolean} **/ @@ -118,7 +118,7 @@ var Selection = function(session) { return this.getRange().isMultiLine(); }; - /** + /** * Gets the current position of the cursor. * @returns {Number} **/ @@ -131,7 +131,7 @@ var Selection = function(session) { * @param {Number} row The new row * @param {Number} column The new column * - * + * **/ this.setSelectionAnchor = function(row, column) { this.anchor.setPosition(row, column); @@ -156,7 +156,7 @@ var Selection = function(session) { }; /** - * + * * Returns an object containing the `row` and `column` of the calling selection lead. * @returns {Object} **/ @@ -168,7 +168,7 @@ var Selection = function(session) { * Shifts the selection up (or down, if [[Selection.isBackwards `isBackwards()`]] is true) the given number of columns. * @param {Number} columns The number of columns to shift by * - * + * * **/ this.shiftSelection = function(columns) { @@ -245,7 +245,7 @@ var Selection = function(session) { * @param {Range} range The range of text to select * @param {Boolean} reverse Indicates if the range should go backwards (`true`) or not * - * + * * @method setSelectionRange * @alias setRange **/ @@ -258,6 +258,8 @@ var Selection = function(session) { this.setSelectionAnchor(range.start.row, range.start.column); this.selectTo(range.end.row, range.end.column); } + if (this.getRange().isEmpty()) + this.$isEmpty = true; this.$desiredColumn = null; }; @@ -274,7 +276,7 @@ var Selection = function(session) { * @param {Number} row The row to select to * @param {Number} column The column to select to * - * + * * **/ this.selectTo = function(row, column) { @@ -287,7 +289,7 @@ var Selection = function(session) { * Moves the selection cursor to the row and column indicated by `pos`. * @param {Object} pos An object containing the row and column * - * + * * **/ this.selectToPosition = function(pos) { @@ -297,7 +299,7 @@ var Selection = function(session) { }; /** - * + * * Moves the selection up one row. **/ this.selectUp = function() { @@ -305,7 +307,7 @@ var Selection = function(session) { }; /** - * + * * Moves the selection down one row. **/ this.selectDown = function() { @@ -313,7 +315,7 @@ var Selection = function(session) { }; /** - * + * * * Moves the selection right one column. **/ @@ -322,7 +324,7 @@ var Selection = function(session) { }; /** - * + * * Moves the selection left one column. **/ this.selectLeft = function() { @@ -330,7 +332,7 @@ var Selection = function(session) { }; /** - * + * * Moves the selection to the beginning of the current line. **/ this.selectLineStart = function() { @@ -338,7 +340,7 @@ var Selection = function(session) { }; /** - * + * * Moves the selection to the end of the current line. **/ this.selectLineEnd = function() { @@ -346,7 +348,7 @@ var Selection = function(session) { }; /** - * + * * Moves the selection to the end of the file. **/ this.selectFileEnd = function() { @@ -354,7 +356,7 @@ var Selection = function(session) { }; /** - * + * * Moves the selection to the start of the file. **/ this.selectFileStart = function() { @@ -362,7 +364,7 @@ var Selection = function(session) { }; /** - * + * * Moves the selection to the first word on the right. **/ this.selectWordRight = function() { @@ -370,14 +372,14 @@ var Selection = function(session) { }; /** - * + * * Moves the selection to the first word on the left. **/ this.selectWordLeft = function() { this.$moveSelection(this.moveCursorWordLeft); }; - /** + /** * Moves the selection to highlight the entire word. * @related EditSession.getWordRange **/ @@ -389,16 +391,16 @@ var Selection = function(session) { } return this.session.getWordRange(row, column); }; - + /** - * + * * Selects an entire word boundary. **/ this.selectWord = function() { this.setSelectionRange(this.getWordRange()); }; - /** + /** * Selects a word, including its right whitespace. * @related EditSession.getAWordRange **/ @@ -433,7 +435,7 @@ var Selection = function(session) { }; /** - * + * * Moves the cursor up one row. **/ this.moveCursorUp = function() { @@ -441,7 +443,7 @@ var Selection = function(session) { }; /** - * + * * Moves the cursor down one row. **/ this.moveCursorDown = function() { @@ -449,7 +451,7 @@ var Selection = function(session) { }; /** - * + * * Moves the cursor left one column. **/ this.moveCursorLeft = function() { @@ -474,7 +476,7 @@ var Selection = function(session) { }; /** - * + * * Moves the cursor right one column. **/ this.moveCursorRight = function() { @@ -499,7 +501,7 @@ var Selection = function(session) { }; /** - * + * * Moves the cursor to the start of the line. **/ this.moveCursorLineStart = function() { @@ -524,7 +526,7 @@ var Selection = function(session) { }; /** - * + * * Moves the cursor to the end of the line. **/ this.moveCursorLineEnd = function() { @@ -543,7 +545,7 @@ var Selection = function(session) { }; /** - * + * * Moves the cursor to the end of the file. **/ this.moveCursorFileEnd = function() { @@ -553,7 +555,7 @@ var Selection = function(session) { }; /** - * + * * Moves the cursor to the start of the file. **/ this.moveCursorFileStart = function() { @@ -561,7 +563,7 @@ var Selection = function(session) { }; /** - * + * * Moves the cursor to the word on the right. **/ this.moveCursorLongWordRight = function() { @@ -607,7 +609,7 @@ var Selection = function(session) { }; /** - * + * * Moves the cursor to the word on the left. **/ this.moveCursorLongWordLeft = function() { @@ -704,11 +706,11 @@ var Selection = function(session) { if (column == line.length) { var l = this.doc.getLength(); - do { + do { row++; rightOfCursor = this.doc.getLine(row) } while (row < l && /^\s*$/.test(rightOfCursor)) - + if (!/^\s+/.test(rightOfCursor)) rightOfCursor = "" column = 0; @@ -729,11 +731,11 @@ var Selection = function(session) { var line = this.session.getLine(row).substring(0, column); if (column == 0) { - do { + do { row--; line = this.doc.getLine(row); } while (row > 0 && /^\s*$/.test(line)) - + column = line.length; if (!/\s+$/.test(line)) line = "" @@ -760,11 +762,11 @@ var Selection = function(session) { }; /** - * Moves the cursor to position indicated by the parameters. Negative numbers move the cursor backwards in the document. + * Moves the cursor to position indicated by the parameters. Negative numbers move the cursor backwards in the document. * @param {Number} rows The number of rows to move by * @param {Number} chars The number of characters to move by * - * + * * @related EditSession.documentToScreenPosition **/ this.moveCursorBy = function(rows, chars) { @@ -790,7 +792,7 @@ var Selection = function(session) { * Moves the selection to the position indicated by its `row` and `column`. * @param {Object} position The position to move to * - * + * **/ this.moveCursorToPosition = function(position) { this.moveCursorTo(position.row, position.column); @@ -802,7 +804,7 @@ var Selection = function(session) { * @param {Number} column The column to move to * @param {Boolean} keepDesiredColumn [If `true`, the cursor move does not respect the previous column]{: #preventUpdateBool} * - * + * **/ this.moveCursorTo = function(row, column, keepDesiredColumn) { // Ensure the row/column is not inside of a fold. @@ -826,7 +828,7 @@ var Selection = function(session) { * @param {Number} column The column to move to * @param {Boolean} keepDesiredColumn {:preventUpdateBool} * - * + * **/ this.moveCursorToScreen = function(row, column, keepDesiredColumn) { var pos = this.session.screenToDocumentPosition(row, column); @@ -861,6 +863,52 @@ var Selection = function(session) { return range; } + this.toJSON = function() { + if (this.rangeCount) { + var data = this.ranges.map(function(r) { + var r1 = r.clone(); + r1.isBackwards = r.cursor == r.start; + return r1; + }); + } else { + var data = this.getRange(); + data.isBackwards = this.isBackwards(); + } + return data; + }; + + this.fromJSON = function(data) { + if (data.start == undefined) { + if (this.rangeList) { + this.toSingleRange(data[0]); + for (var i = data.length; i--; ) { + var r = Range.fromPoints(data[i].start, data[i].end); + if (data.isBackwards) + r.cursor = r.start; + this.addRange(r, true); + } + return; + } else + data = data[0]; + } + if (this.rangeList) + this.toSingleRange(data); + this.setSelectionRange(data, data.isBackwards); + }; + + this.isEqual = function(data) { + if ((data.length || this.rangeCount) && data.length != this.rangeCount) + return false; + if (!data.length || !this.ranges) + return this.getRange().isEqual(data); + + for (var i = this.ranges.length; i--; ) { + if (!this.ranges[i].isEqual(data[i])) + return false + } + return true; + } + }).call(Selection.prototype); exports.Selection = Selection; diff --git a/lib/ace/selection_test.js b/lib/ace/selection_test.js index b69fd6dd..e1a67669 100644 --- a/lib/ace/selection_test.js +++ b/lib/ace/selection_test.js @@ -434,9 +434,7 @@ module.exports = { }, "test (keyboard navigation) when curLine is not EOL and targetLine is all whitespace new column should be current column": function() { - var session = new EditSession("function (a) {\n\ - \n\ -}"); + var session = new EditSession("function (a) {\n \n}"); var selection = session.getSelection(); selection.moveCursorTo(2, 0); @@ -445,16 +443,33 @@ module.exports = { assert.position(selection.getCursor(), 1, 0); }, - "test (keyboard navigation) when curLine is EOL and targetLine is shorter dan current column, new column should be targetLine's EOL": function() { - var session = new EditSession("function (a) {\n\ - \n\ -}"); + "test (keyboard navigation) when curLine is EOL and targetLine is shorter than current column, new column should be targetLine's EOL": function() { + var session = new EditSession("function (a) {\n \n}"); var selection = session.getSelection(); selection.moveCursorTo(0, 14); selection.moveCursorDown(); assert.position(selection.getCursor(), 1, 4); + }, + + "test fromJSON/toJSON": function() { + var session = new EditSession("function (a) {\n \n}"); + var selection = session.getSelection(); + + selection.moveCursorTo(0, 14); + selection.moveCursorDown(); + assert.position(selection.getCursor(), 1, 4); + var data = selection.toJSON(); + data = JSON.parse(JSON.stringify(data)) + selection.moveCursorDown(); + assert.position(selection.getCursor(), 2, 1); + + assert.ok(!selection.isEqual(data)); + + selection.fromJSON(data); + assert.position(selection.getCursor(), 1, 4); + assert.ok(selection.isEqual(data)); } }; diff --git a/lib/ace/undomanager.js b/lib/ace/undomanager.js index 0a226d5e..12aaa01d 100644 --- a/lib/ace/undomanager.js +++ b/lib/ace/undomanager.js @@ -63,6 +63,9 @@ var UndoManager = function() { this.execute = function(options) { var deltas = options.args[0]; this.$doc = options.args[1]; + if (options.merge && this.hasUndo()){ + deltas = this.$undoStack.pop().concat(deltas); + } this.$undoStack.push(deltas); this.$redoStack = [];