Drafts Action Directory

Move Lines Up

Posted by @mattgemmell, Last updated: 2018-07-04 12:40:23 UTC

Moves the selected line(s) up by one line.

Install

Steps

  • script

    // Direction to move the lines.
    var moveUp = true;
    
    String.prototype.splitLines = function () {
    	// Splits string into lines, each ending with a newline (\n).
    	// Note: the last fragment might not end with a newline.
    	return this.match(/[^\n]*\n/g);
    };
    
    function minRange(theRange) {
    	// Returns index of start of theRange.
    	return theRange[0];
    }
    
    function maxRange(theRange) {
    	// Returns index immediately AFTER end of theRange.
    	return theRange[0] + theRange[1];
    }
    
    String.prototype.lastChar = function () {
    	return this.substring(this.length - 1, this.length);
    };
    
    String.prototype.inRange = function (theRange) {
    	return this.substring(theRange[0], theRange[0] + theRange[1]);
    }
    
    function show(title, content) {
    	alert(title + "\n#" + content + "#");
    }
    
    function showInRange(title, range, text) {
    	show(title, text.substring(range[0], range[0] + range[1]));
    }
    
    /*
    We need to distinguish between a user-selection which spans a newLine onto the following line, and a getSelectedLineRange() result which has automatically expanded to include a line's trailing newline.
    
    This is for the case where getSelectedLineRange() is ambiguous:
    
    - This occurs only when a user selection exists. If no selection exists, the insertion point is always on a single line.
    - This occurs only when the user selection ends immediately after a newLine. Otherwise, there's no ambiguity in the range returned from getSelectedLineRange().
    - It doesn't matter whether the following line is blank or not.
    - This doesn't occur with a _preceding_ selected blank line, because getSelectedLineRange() will be different.
    
    The ambiguity in getSelectedLineRange() arises because:
    
    A. The getSelectedLineRange() might span the trailing linebreak because **the user selected past that point**.
    B. The getSelectedLineRange() might span the trailing linebreak because **it auto-expands to include it**.
    
    In situation A, the user wants to **also act on the following line**; they selected it explicitly.
    In situation B, the user wants **not to act on the following line**; its preceding newline was just part of the line-range.
    
    We can disambiguate by checking getSelectedLineRange() against getSelectedRange(). If both ranges end at the same maxRange, it's a user-selection, and we **should** include the following line (by expanding the candidate line-range up to and including the next newline). Otherwise, the range was auto-expanded to include the trailing newline, and we should **not** include the following line.
    */
    
    // Do some sanity checking.
    var newLine = "\n";
    var lineRange = editor.getSelectedLineRange();
    var selRange = editor.getSelectedRange();
    var draftText = editor.getText();
    var draftLength = draftText.length;
    var proceed = true;
    if (moveUp && minRange(lineRange) == 0) {
    	// Can't move lines up.
    	proceed = false;
    	
    } else if (!moveUp && maxRange(lineRange) >= draftLength) {
    	// Can't move lines down.
    	proceed = false;
    }
    
    if (proceed) {
    	// Normalise, to ensure all lines end with a newLine.
    	var addedNewLine = false;
    	if (draftText.lastChar() != newLine) {
    		draftText = draftText + newLine;
    		addedNewLine = true;
    		if (moveUp) {
    			// Check to see if we need to adjust lineRange accordingly.
    			// Doesn't apply when moving down, because we can't be on the last line.
    			if (maxRange(lineRange) == draftLength) {
    				// Last line of draft is selected. Expand length of range for added newLine.
    				lineRange[1] = lineRange[1] + 1;
    			}
    		}
    		draftLength = draftLength + 1;
    	}
    
    	// Handle ambiguous line-range case of user-selection spanning a trailing newLine.
    	// (See full explanation above.)
    	if (selRange[1] > 0) {
    		// There's a selection.
    		var selText = draftText.inRange(selRange);
    		if (selText.lastChar() == newLine) {
    			// Last char of user selection is a newline; expand lineRange to include next line.
    			// (Because visually in terms of the iOS selection, that's the user's intention.)
    			// Note: also include the newLine itself at the end of the next line.
    			var nextNewLinePosn = draftText.indexOf(newLine, maxRange(selRange));
    			lineRange[1] = lineRange[1] + (nextNewLinePosn - maxRange(lineRange)) + 1;
    		}
    	}
    	
    	// Expand lineRange to give us an extra line to move.
    	if (moveUp) {
    		// Moving up. Get extra line before the selected lines, to move down.
    		// We offset by -2 to go back past the preceding lineBreak.
    		var prevLineStart = draftText.lastIndexOf(newLine, minRange(lineRange) - 2);
    		
    		// Adjust prevLineStart so we don't capture the found newline itself.
    		if (prevLineStart == -1) { // In case we hit start of draft.
    			prevLineStart = 0;
    		} else {
    			// Exclude the newLine itself which preceeds the previous line.
    			if ((minRange(lineRange) - prevLineStart) > 1) {
    				prevLineStart = prevLineStart + 1;
    			}
    		}
    		lineRange[1] = lineRange[1] + (lineRange[0] - prevLineStart);
    		lineRange[0] = prevLineStart;
    		
    	} else {
    		// Moving down. Get extra line after the selected lines, to move up.
    		var nextLineEnd = draftText.indexOf(newLine, maxRange(lineRange));
    		if (nextLineEnd == -1) { // We hit end of draft.
    			nextLineEnd = draftLength;
    		}
    		lineRange[1] = (nextLineEnd + 1) - lineRange[0];
    	}
    	
    	// Grab the relevant full lines of text.
    	var selectedLines = draftText.substring(minRange(lineRange), maxRange(lineRange));
    	
    	// Rearrange lines.
    	var lines = selectedLines.splitLines();
    	var extraLineArray = [];
    	if (moveUp) {
    		// Shift first (extra) line to end, thus moving all others up.
    		extraLineArray = lines.splice(0, 1);
    		extraLineArray = lines.splice(lines.length, 0, extraLineArray[0]);
    		
    	} else {
    		// Shift last (extra) line to start, thus moving all others down.
    		extraLineArray = lines.splice(lines.length - 1, 1);
    		extraLineArray = lines.splice(0, 0, extraLineArray[0]);
    	}
    	
    	// Replace selected lines with reordered ones.
    	var newLinesChunk = lines.join("");
    
    	// If we added a newLine to the end of draftText originally,
    	// and we're replacing in that portion, remove it again.
    	if (addedNewLine && maxRange(lineRange) >= (draftLength - 1)) {
    		newLinesChunk = newLinesChunk.substring(0, newLinesChunk.length - 1);
    	}
    	
    	// Work out new selection range, to preserve selected lines.
    	var newSelectionRange = [ lineRange[0], lineRange[1] ];
    	newSelectionRange[1] = newLinesChunk.length;
    	var delta = 0;
    	if (moveUp) {
    		// Should select all but the last (extra; moved down) line.
    		delta = lines[lines.length - 1].length;
    		newSelectionRange[1] = newSelectionRange[1] - delta;
    		
    	} else {
    		// Should select all but the first (extra; moved up) line.
    		delta = lines[0].length;
    		newSelectionRange[0] = newSelectionRange[0] + delta;
    		newSelectionRange[1] = newSelectionRange[1] - delta;
    	}
    
    	// Replace text in editor.
    	editor.setTextInRange(lineRange[0], lineRange[1], newLinesChunk);
    	
    	// Maintain selection, ensuring it doesn't end in a spurious newLine.
    	if (editor.getTextInRange(newSelectionRange[0], newSelectionRange[1]).lastChar() == newLine) {
    		newSelectionRange[1] = newSelectionRange[1] - 1;
    	}
    	editor.setSelectedRange(newSelectionRange[0], newSelectionRange[1]);
    }
    
    /*
    
    Test data below.
    
    one
    two
    three
    four
    
    */

Comments

Actions available in the Action Directory are uploaded by community members. Use appropriate caution reviewing downloaded actions before use.