indentation.js (5224B)
1 /* 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 "use strict"; 7 8 const EXPAND_TAB = "devtools.editor.expandtab"; 9 const TAB_SIZE = "devtools.editor.tabsize"; 10 const DETECT_INDENT = "devtools.editor.detectindentation"; 11 const DETECT_INDENT_MAX_LINES = 500; 12 13 /** 14 * Get the number of indentation units to use to indent a "block" 15 * and a boolean indicating whether indentation must be done using tabs. 16 * 17 * @return {object} an object of the form {indentUnit, indentWithTabs}. 18 * |indentUnit| is the number of indentation units to use 19 * to indent a "block". 20 * |indentWithTabs| is a boolean which is true if indentation 21 * should be done using tabs. 22 */ 23 function getTabPrefs() { 24 const indentWithTabs = !Services.prefs.getBoolPref(EXPAND_TAB); 25 const indentUnit = Services.prefs.getIntPref(TAB_SIZE, 2); 26 return { indentUnit, indentWithTabs }; 27 } 28 29 /** 30 * Get the indentation to use in an editor, or return false if the user has 31 * asked for the indentation to be guessed from some text. 32 * 33 * @return {false | object} 34 * Returns false if the "detect indentation" pref is set. 35 * If the pref is not set, returns an object of the same 36 * form as returned by getTabPrefs. 37 */ 38 function getIndentationFromPrefs() { 39 const shouldDetect = Services.prefs.getBoolPref(DETECT_INDENT); 40 if (shouldDetect) { 41 return false; 42 } 43 44 return getTabPrefs(); 45 } 46 47 /** 48 * Given a function that can iterate over some text, compute the indentation to 49 * use. This consults various prefs to arrive at a decision. 50 * 51 * @param {Function} iterFunc A function of three arguments: 52 * (start, end, callback); where |start| and |end| describe 53 * the range of text lines to examine, and |callback| is a function 54 * to be called with the text of each line. 55 * 56 * @return {object} an object of the form {indentUnit, indentWithTabs}. 57 * |indentUnit| is the number of indentation units to use 58 * to indent a "block". 59 * |indentWithTabs| is a boolean which is true if indentation 60 * should be done using tabs. 61 */ 62 function getIndentationFromIteration(iterFunc) { 63 let indentWithTabs = !Services.prefs.getBoolPref(EXPAND_TAB); 64 let indentUnit = Services.prefs.getIntPref(TAB_SIZE); 65 const shouldDetect = Services.prefs.getBoolPref(DETECT_INDENT); 66 67 if (shouldDetect) { 68 const indent = detectIndentation(iterFunc); 69 if (indent != null) { 70 indentWithTabs = indent.tabs; 71 indentUnit = indent.spaces ? indent.spaces : indentUnit; 72 } 73 } 74 75 return { indentUnit, indentWithTabs }; 76 } 77 78 /** 79 * A wrapper for @see getIndentationFromIteration which computes the 80 * indentation of a given string. 81 * 82 * @param {string} string the input text 83 * @return {object} an object of the same form as returned by 84 * getIndentationFromIteration 85 */ 86 function getIndentationFromString(string) { 87 const iteratorFn = function (start, end, callback) { 88 const split = string.split(/\r\n|\r|\n|\f/); 89 split.slice(start, end).forEach(callback); 90 }; 91 return getIndentationFromIteration(iteratorFn); 92 } 93 94 /** 95 * Detect the indentation used in an editor. Returns an object 96 * with 'tabs' - whether this is tab-indented and 'spaces' - the 97 * width of one indent in spaces. Or `null` if it's inconclusive. 98 */ 99 function detectIndentation(textIteratorFn) { 100 // # spaces indent -> # lines with that indent 101 const spaces = {}; 102 // indentation width of the last line we saw 103 let last = 0; 104 // # of lines that start with a tab 105 let tabs = 0; 106 // # of indented lines (non-zero indent) 107 let total = 0; 108 109 textIteratorFn(0, DETECT_INDENT_MAX_LINES, text => { 110 if (text.startsWith("\t")) { 111 tabs++; 112 total++; 113 return; 114 } 115 let width = 0; 116 while (text[width] === " ") { 117 width++; 118 } 119 // don't count lines that are all spaces 120 if (width == text.length) { 121 last = 0; 122 return; 123 } 124 if (width > 1) { 125 total++; 126 } 127 128 // see how much this line is offset from the line above it 129 const indent = Math.abs(width - last); 130 if (indent > 1 && indent <= 8) { 131 spaces[indent] = (spaces[indent] || 0) + 1; 132 } 133 last = width; 134 }); 135 136 // this file is not indented at all 137 if (total == 0) { 138 return null; 139 } 140 141 // mark as tabs if they start more than half the lines 142 if (tabs >= total / 2) { 143 return { tabs: true }; 144 } 145 146 // find most frequent non-zero width difference between adjacent lines 147 let freqIndent = null, 148 max = 1; 149 for (let width in spaces) { 150 width = parseInt(width, 10); 151 const tally = spaces[width]; 152 if (tally > max) { 153 max = tally; 154 freqIndent = width; 155 } 156 } 157 if (!freqIndent) { 158 return null; 159 } 160 161 return { tabs: false, spaces: freqIndent }; 162 } 163 164 exports.EXPAND_TAB = EXPAND_TAB; 165 exports.TAB_SIZE = TAB_SIZE; 166 exports.DETECT_INDENT = DETECT_INDENT; 167 exports.getTabPrefs = getTabPrefs; 168 exports.getIndentationFromPrefs = getIndentationFromPrefs; 169 exports.getIndentationFromIteration = getIndentationFromIteration; 170 exports.getIndentationFromString = getIndentationFromString;