diff options
Diffstat (limited to 'devtools/shared/indentation.js')
-rw-r--r-- | devtools/shared/indentation.js | 170 |
1 files changed, 170 insertions, 0 deletions
diff --git a/devtools/shared/indentation.js b/devtools/shared/indentation.js new file mode 100644 index 0000000000..3dfd1a088d --- /dev/null +++ b/devtools/shared/indentation.js @@ -0,0 +1,170 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPAND_TAB = "devtools.editor.expandtab"; +const TAB_SIZE = "devtools.editor.tabsize"; +const DETECT_INDENT = "devtools.editor.detectindentation"; +const DETECT_INDENT_MAX_LINES = 500; + +/** + * Get the number of indentation units to use to indent a "block" + * and a boolean indicating whether indentation must be done using tabs. + * + * @return {Object} an object of the form {indentUnit, indentWithTabs}. + * |indentUnit| is the number of indentation units to use + * to indent a "block". + * |indentWithTabs| is a boolean which is true if indentation + * should be done using tabs. + */ +function getTabPrefs() { + const indentWithTabs = !Services.prefs.getBoolPref(EXPAND_TAB); + const indentUnit = Services.prefs.getIntPref(TAB_SIZE); + return { indentUnit, indentWithTabs }; +} + +/** + * Get the indentation to use in an editor, or return false if the user has + * asked for the indentation to be guessed from some text. + * + * @return {false | Object} + * Returns false if the "detect indentation" pref is set. + * If the pref is not set, returns an object of the same + * form as returned by getTabPrefs. + */ +function getIndentationFromPrefs() { + const shouldDetect = Services.prefs.getBoolPref(DETECT_INDENT); + if (shouldDetect) { + return false; + } + + return getTabPrefs(); +} + +/** + * Given a function that can iterate over some text, compute the indentation to + * use. This consults various prefs to arrive at a decision. + * + * @param {Function} iterFunc A function of three arguments: + * (start, end, callback); where |start| and |end| describe + * the range of text lines to examine, and |callback| is a function + * to be called with the text of each line. + * + * @return {Object} an object of the form {indentUnit, indentWithTabs}. + * |indentUnit| is the number of indentation units to use + * to indent a "block". + * |indentWithTabs| is a boolean which is true if indentation + * should be done using tabs. + */ +function getIndentationFromIteration(iterFunc) { + let indentWithTabs = !Services.prefs.getBoolPref(EXPAND_TAB); + let indentUnit = Services.prefs.getIntPref(TAB_SIZE); + const shouldDetect = Services.prefs.getBoolPref(DETECT_INDENT); + + if (shouldDetect) { + const indent = detectIndentation(iterFunc); + if (indent != null) { + indentWithTabs = indent.tabs; + indentUnit = indent.spaces ? indent.spaces : indentUnit; + } + } + + return { indentUnit, indentWithTabs }; +} + +/** + * A wrapper for @see getIndentationFromIteration which computes the + * indentation of a given string. + * + * @param {String} string the input text + * @return {Object} an object of the same form as returned by + * getIndentationFromIteration + */ +function getIndentationFromString(string) { + const iteratorFn = function (start, end, callback) { + const split = string.split(/\r\n|\r|\n|\f/); + split.slice(start, end).forEach(callback); + }; + return getIndentationFromIteration(iteratorFn); +} + +/** + * Detect the indentation used in an editor. Returns an object + * with 'tabs' - whether this is tab-indented and 'spaces' - the + * width of one indent in spaces. Or `null` if it's inconclusive. + */ +function detectIndentation(textIteratorFn) { + // # spaces indent -> # lines with that indent + const spaces = {}; + // indentation width of the last line we saw + let last = 0; + // # of lines that start with a tab + let tabs = 0; + // # of indented lines (non-zero indent) + let total = 0; + + textIteratorFn(0, DETECT_INDENT_MAX_LINES, text => { + if (text.startsWith("\t")) { + tabs++; + total++; + return; + } + let width = 0; + while (text[width] === " ") { + width++; + } + // don't count lines that are all spaces + if (width == text.length) { + last = 0; + return; + } + if (width > 1) { + total++; + } + + // see how much this line is offset from the line above it + const indent = Math.abs(width - last); + if (indent > 1 && indent <= 8) { + spaces[indent] = (spaces[indent] || 0) + 1; + } + last = width; + }); + + // this file is not indented at all + if (total == 0) { + return null; + } + + // mark as tabs if they start more than half the lines + if (tabs >= total / 2) { + return { tabs: true }; + } + + // find most frequent non-zero width difference between adjacent lines + let freqIndent = null, + max = 1; + for (let width in spaces) { + width = parseInt(width, 10); + const tally = spaces[width]; + if (tally > max) { + max = tally; + freqIndent = width; + } + } + if (!freqIndent) { + return null; + } + + return { tabs: false, spaces: freqIndent }; +} + +exports.EXPAND_TAB = EXPAND_TAB; +exports.TAB_SIZE = TAB_SIZE; +exports.DETECT_INDENT = DETECT_INDENT; +exports.getTabPrefs = getTabPrefs; +exports.getIndentationFromPrefs = getIndentationFromPrefs; +exports.getIndentationFromIteration = getIndentationFromIteration; +exports.getIndentationFromString = getIndentationFromString; |