aboutsummaryrefslogtreecommitdiff
path: root/data/plugins/detectindent.lua
blob: d323adb5ea1e8bd0752d3433a98ae41784593f20 (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
local core = require "core"
local command = require "core.command"
local common = require "core.common"
local config = require "core.config"
local DocView = require "core.docview"
local Doc = require "core.doc"
local tokenizer = require "core.tokenizer"

local cache = setmetatable({}, { __mode = "k" })


local function add_to_stat(stat, val)
  for i = 1, #stat do
    if val == stat[i][1] then
      stat[i][2] = stat[i][2] + 1
      return
    end
  end
  stat[#stat + 1] = {val, 1}
end


local function optimal_indent_from_stat(stat)
  if #stat == 0 then return nil, 0 end
  local bins = {}
  for k = 1, #stat do
    local indent = stat[k][1]
    local score = 0
    local mult_prev, lines_prev
    for i = k, #stat do
      if stat[i][1] % indent == 0 then
        local mult = stat[i][1] / indent
        if not mult_prev or (mult_prev + 1 == mult and lines_prev / stat[i][2] > 0.1) then
          -- we add the number of lines to the score only if the previous
          -- multiple of "indent" was populated with enough lines.
          score = score + stat[i][2]
        end
        mult_prev, lines_prev = mult, stat[i][2]
      end
    end
    bins[#bins + 1] = {indent, score}
  end
  table.sort(bins, function(a, b) return a[2] > b[2] end)
  return bins[1][1], bins[1][2]
end


-- return nil if it is a comment or blank line or the initial part of the
-- line otherwise.
-- we don't need to have the whole line to detect indentation.
local function get_first_line_part(tokens)
  local i, n = 1, #tokens
  while i + 1 <= n do
    local ttype, ttext = tokens[i], tokens[i + 1]
    if ttype ~= "comment" and ttext:gsub("%s+", "") ~= "" then
      return ttext
    end
    i = i + 2
  end
end

local function get_non_empty_lines(syntax, lines)
  return coroutine.wrap(function()
    local tokens, state
    local i = 0
    for _, line in ipairs(lines) do
      tokens, state = tokenizer.tokenize(syntax, line, state)
      local line_start = get_first_line_part(tokens)
      if line_start then
        i = i + 1
        coroutine.yield(i, line_start)
      end
    end
  end)
end


local auto_detect_max_lines = 200

local function detect_indent_stat(doc)
  local stat = {}
  local tab_count = 0
  for i, text in get_non_empty_lines(doc.syntax, doc.lines) do
    local str = text:match("^ %s+%S")
    if str then add_to_stat(stat, #str - 1) end
    local str = text:match("^\t+")
    if str then tab_count = tab_count + 1 end
    -- Stop parsing when files is very long. Not needed for euristic determination.
    if i > auto_detect_max_lines then break end
  end
  table.sort(stat, function(a, b) return a[1] < b[1] end)
  local indent, score = optimal_indent_from_stat(stat)
  if tab_count > score then
    return "hard", config.indent_size, tab_count
  else
    return "soft", indent or config.indent_size, score or 0
  end
end


local doc_text_input = Doc.text_input
local adjust_threshold = 4

local function update_cache(doc)
  local type, size, score = detect_indent_stat(doc)
  cache[doc] = { type = type, size = size, confirmed = (score >= adjust_threshold) }
  doc.indent_info = cache[doc]
  if score < adjust_threshold and Doc.text_input == doc_text_input then
    Doc.text_input = function(self, ...)
      doc_text_input(self, ...)
      update_cache(self)
    end
  elseif score >= adjust_threshold and Doc.text_input ~= doc_text_input then
    Doc.text_input = doc_text_input
  end
end


local new = Doc.new
function Doc:new(...)
  new(self, ...)
  update_cache(self)
end

local clean = Doc.clean
function Doc:clean(...)
  clean(self, ...)
  update_cache(self)
end


local function with_indent_override(doc, fn, ...)
  local c = cache[doc]
  if not c then
    return fn(...)
  end
  local type, size = config.tab_type, config.indent_size
  config.tab_type, config.indent_size = c.type, c.size or config.indent_size
  local r1, r2, r3 = fn(...)
  config.tab_type, config.indent_size = type, size
  return r1, r2, r3
end


local perform = command.perform
function command.perform(...)
  return with_indent_override(core.active_view.doc, perform, ...)
end


local draw = DocView.draw
function DocView:draw(...)
  return with_indent_override(self.doc, draw, self, ...)
end