summaryrefslogtreecommitdiffstats
path: root/devtools/shared/indentation.js
blob: 3dfd1a088da5ac93f942941d3a8a44a80d8b07d3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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;