JMichaelTX
1/14/2015 - 6:55 PM

Outdent selected line(s) in OS X 10.10 Script Editor

Outdent selected line(s) in OS X 10.10 Script Editor

// ROBIN TREW 2015 MIT License

// OSX 10.10 SCRIPT EDITOR – REDUCE INDENTATION OF SELECTED LINES (by Tab or 4 spaces)

// Add remove '// ' from before printing characters

// Depends on a library .scpt being saved at ~/Library/Script Libraries/DraftsSE.scpt
// See: See: http://support.foldingtext.com/t/writing-scripts-which-run-on-both-foldingtext-and-ios-drafts-4/652/7

// INCLUDE ALL OR PART OF LIBRARY OF IOS DRAFTS-COMPATIBLE EDITOR FUNCTIONS:
function run() {

	// PART 1: CODE SPECIFIC TO YOSEMITE SCRIPT EDITOR:

	// SE implmentations of the basic iOS Drafts functions
	// (Implementations of these functions for FoldingText and other editors with .js scripting at:
	// 
	// getText()                               getSelectedLineRange()
	// setText(string)                         getSelectedRange()
	// getSelectedText()                       setSelectedRange(start, length)
	// setSelectedText(string)                 getClipboard()
	// getTextInRange(start, length)           setClipboard(string)
	// setTextInRange(start, length, string)


	var app = Application("Script Editor"),
		lstDoc = app.documents(),
		oDoc = lstDoc.length ? lstDoc[0] : null;

	if (!oDoc) return false;

	//either var drafts = Library('DraftsSE'); // Assuming file saved at: ~/Library/Script Libraries/DraftsSE.scpt
	// or locally:
	drafts = {
		getTextInRange: function (iStart, iLength) {
			if (oDoc) return oDoc.text().substring(iStart, iStart + iLength);
		},
		setSelectedRange: function (iStart, iLength) {
			if (oDoc) {
				oDoc.selection = oDoc.text.characters.slice(iStart, iStart + iLength);
			}
		},
		getSelectedLineRange: function () {
			// works, but let me know if you see a shorter route :-)
			var rgxLFCR=/[\r\n]/, dct, strFull, iFrom, iTo, lngChars;
			if (oDoc) {
				dct = oDoc.selection.characterRange();
				strFull = oDoc.text();
				lngChars = strFull.length;
				iFrom = dct.x;
				while (iFrom--)
					if (strFull[iFrom].match(rgxLFCR)) break;
				iFrom += 1;
				iTo = dct.y - 1;
				while (iTo++ < lngChars)
					if (strFull[iTo].match(rgxLFCR)) break;
				return [iFrom, (iTo - iFrom)];
			}
		},
		setTextInRange: function (iStart, iLength, strText) {
			if (oDoc) { // record the existing selection coordinates
				var lngDelta = (strText.length - iLength) - 2,
					oSeln = oDoc.selection,
					dct = oSeln.characterRange(),
					iFrom = dct.x,
					iTo = dct.y;

				try { // use the selection to edit elsewhere, then restore with any adjustment
					oDoc.selection = oDoc.text.characters.slice(iStart, iStart + iLength);
					oSeln.contents = strText;
					oDoc.selection = oDoc.text.characters.slice(
						(iStart < iFrom) ? iFrom + lngDelta : iFrom, ((iStart + iLength) < iTo) ? iTo + lngDelta : iTo
					);
				} catch (e) {}
			}
		}
	};

	// -------------------------------------------------------------------	

	// PART 2: CODE COMPATIBLE WITH FOLDINGTEXT, DRAFTS, 1WRITER, TEXTWELL

	function outdentSelectedLines() {
		var strDefaultIndent = '\\t', // tab must be preceded by two backslashes
			strIndent,
			rgxLeftSpace = /^([ \t]+)(?=\S)/gm,
			rgxOut,
			lstLinesFromTo = drafts.getSelectedLineRange(),
			iFrom = lstLinesFromTo[0],
			lngSelnChars = lstLinesFromTo[1],
			strLines = drafts.getTextInRange(iFrom, lngSelnChars),
			oMatch = rgxLeftSpace.exec(strLines),
			strGap = oMatch ? oMatch[1] : null,
			i = strGap ? strGap.length : 0,
			iTab = 0,
			iSpace = 0;

		// INDENTED WITH SPACES OR WITH TABS ?
		if (strGap) {
			while (i--) {
				if (strGap[i] === '\t') iTab++;
				else iSpace++;
			}
			strIndent = (iTab > iSpace) ? '\\t' : '    ';
		} else strIndent = strDefaultIndent;

		// BUILD THE OUT-DENTED TEXT
		rgxOut = new RegExp('^' + strIndent, 'gm');
		strNew = strLines.replace(rgxOut, '');

		// PLACE AND SELECT
		drafts.setTextInRange(iFrom, lngSelnChars-1, strNew);
		drafts.setSelectedRange(iFrom, strNew.length - 1);
		return true;
	}

	// MAIN
	outdentSelectedLines();
}