File size: 9,182 Bytes
b6a38d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
-- Small XTextEditor plugins should be implemented here


----- XSpellcheckPlugin

DefineClass.XSpellcheckPlugin = {
	__parents = { "XTextEditorPlugin" },
	UnderlineColor = RGB(255, 20, 20),
}

local word_chars = "[_%w\127-\255\39]" -- count any utf8 extented character as a word character
local nonword_chars = "[^_%w\127-\255\39]"
local word_pattern = "(" .. word_chars .. "*)" .. "(" .. nonword_chars .. "*)"
local whitespace = "[\9\32]"
local multiple_whitespace = "[\9\32][\9\32]+"

local MeasureToCharStart = UIL.MeasureToCharStart
local StretchText = UIL.StretchText

function XSpellcheckPlugin:Init()
	LoadDictionary()
end

function XSpellcheckPlugin:OnDrawText(edit, line_idx, text, target_box, font, text_color)
	local y = target_box:maxy() - 4
	local position_in_text = 0
	local substitutions = { {"β€˜", "'"}, {"’", "'"}, {"β€œ", "\""}, {"”", "\""}, {"–", "-"}, {"β€”", "-"}, {"…", "."}, {"βˆ’", "-"}}
	for i = 1, #substitutions do
		text = text:gsub(substitutions[i][1], substitutions[i][2])
	end
	-- underline whitespace at the begining of the line
	if #text >= 1 and text:sub(1,1):find(whitespace) then
		self:Underline(text, target_box:minx(), y, 1, 2, font)
	end
	-- underline whitespace at the end of the line; lines always end with a space, so the second to last character in the string should be checked
	if #text >= 1 and text:sub(#text - 1,#text - 1):find(whitespace) then
		self:Underline(text, target_box:minx(), y, #text- 1, #text, font)
	end
	local untagged, tag, first, last = string.nexttag(text, 1)
	
	-- Check for unmatched closing tag bracket (>) at the beginning of the text and if found, skip the dictionary checks for the text before it
	local unmatched_closing_tag = false
	local close_tag_idx
	if not tag then
		local open_tag_idx = string.find(untagged, "<")
		close_tag_idx = string.find(untagged, ">")
		
		-- Find the farthest unmatched closing tag bracket (>)
		while open_tag_idx and close_tag_idx do
			local next_close_tag = string.find(untagged, ">", close_tag_idx + 1)
			if next_close_tag and next_close_tag < open_tag_idx then
				close_tag_idx = next_close_tag
			else
				break
			end
		end
		
		unmatched_closing_tag = (close_tag_idx and not open_tag_idx) or (close_tag_idx and open_tag_idx and close_tag_idx < open_tag_idx)
	end
	
	-- try this - if the current position_in_text is < and there's no closing tag until the end skip/break
	for word, non_word in text:gmatch(word_pattern) do
		-- Check for unmatched opening tag bracket (<) at the end of the text and if found, skip the dictionary checks for the text after it
		if string.sub(text, position_in_text, position_in_text) == "<" and not string.find(text, ">", position_in_text + 1) then
			break
		end
		
		local current_word_end_pos = position_in_text + utf8.len(word)
		local new_position_in_text = current_word_end_pos + utf8.len(non_word)
		if tag and position_in_text >= first and current_word_end_pos <= last then
			-- we have reached a word/text enclosed in < and > - don't check it against the dictionary
			if position_in_text < last and new_position_in_text >= last then
				-- find the next word in tags
				untagged, tag, first, last = string.nexttag(text, new_position_in_text)
			end
		-- Enter only if we're outside of the unmatched closing tag (if any)
		elseif not (unmatched_closing_tag and position_in_text <= close_tag_idx) then
			word = word:gsub("^'", ""):gsub("'$", "") -- remove leading and trailing ' characters
			local lowercase_word = word:lower()
			if not WordInDictionary(word, lowercase_word) then
				self:Underline(text, target_box:minx(), y, position_in_text + 1, current_word_end_pos + 1, font)
			end
			local comma_pos = non_word:find("[,;].*")
			local comma, other_chars = non_word:match("([,;])(.*)")
			-- , or ; is underlined only if it is not followed by space and is at the end of line
			-- however, every line ends with a space and if there's a space after the , or ;, it needs to be checked for eol
			if comma and (not other_chars or not other_chars:starts_with("<")) and 
				(not other_chars or other_chars:sub(1, 1) ~= " " or
				 (other_chars:sub(1, 1) == " " and current_word_end_pos + comma_pos ==  text:len()))
			then
				local underline_start = current_word_end_pos + comma_pos
				self:Underline(text, target_box:minx(), y, underline_start, underline_start + 1, font)
			end
			-- . or : is underlined only if it is not followed by space
			local dot_pos = non_word:find("[%.:][^%s]*$")
			local dot_idx = dot_pos and current_word_end_pos + dot_pos
			if dot_idx and dot_idx ~= text:len() then
				local next_char = text:sub(dot_idx + 1, dot_idx + 1)
				if next_char ~= "\"" and next_char ~= "\'" and next_char ~= "." and next_char ~= "<" then
					self:Underline(text, target_box:minx(), y, dot_idx, dot_idx + 1, font)
				end
			end
			-- multiple whitespace handling
			local whitespace_start, whitespace_end = non_word:find(multiple_whitespace)
			if whitespace_start then
				self:Underline(text, target_box:minx(), y, current_word_end_pos + whitespace_start, current_word_end_pos + whitespace_end + 1, font)
			end
		end
		position_in_text = new_position_in_text
	end
end

function XSpellcheckPlugin:Underline(text, x, y, underline_start, underline_end, font)
	local x_start = MeasureToCharStart(text, font, underline_start)
	local x_end = MeasureToCharStart(text, font, underline_end)			
	UIL.DrawSolidRect(box(x + x_start, y, x + x_end, y + 2), self.UnderlineColor)
end

function XSpellcheckPlugin:OnRightButtonDown(edit, pt)
	local word = edit:GetWordUnderCursor(pt) or ""
	local lowercase_word = word:lower()
	if not WordInDictionary(word, lowercase_word) then
		CreateRealTimeThread(function()
			local title = "Add to dictionary"
			local text = "Do you want to save '"..word.."' to the dictionary?"
			local dialog = StdMessageDialog:new({}, edit.desktop, { question = true, title = title, text = text })
			dialog:Open()
			dialog:SetZOrder(BaseLoadingScreen.ZOrder + 2) -- above the bug report dialog...
			local result, win = dialog:Wait()
			if result == "ok" then
				local new_entry = word:match("%l") and lowercase_word or word
				SpellcheckDict[new_entry] = true
				WriteToDictionary(SpellcheckDict)
			end
		end)
		return "break"
	end
end


----- XExternalTextEditorPlugin

if FirstLoad then
	g_ExternalTextEditorActiveCtrl = false
end

DefineClass.XExternalTextEditorPlugin = {
	__parents = { "XTextEditorPlugin" },
}

function XExternalTextEditorPlugin:Init() 
	g_ExternalTextEditorActiveCtrl = false	
end

function XExternalTextEditorPlugin:Done() 
	if g_ExternalTextEditorActiveCtrl then
		g_ExternalTextEditorActiveCtrl = false
	end
end

function XExternalTextEditorPlugin:OpenEditor(edit)
	g_ExternalTextEditorActiveCtrl = edit
	AsyncCreatePath("AppData/editorplugin/")
	local file_path = "AppData/editorplugin/" .. config.DefaultExternalTextEditorTempFile
	AsyncStringToFile(file_path, edit:GetText())
	local cmd = config.DefaultExternelTextEditorCmd  
					and string.format(config.DefaultExternelTextEditorCmd, ConvertToOSPath(file_path))
					or string.format("\"%s\" %s",config.DefaultExternelTextEditorPath, ConvertToOSPath(file_path))
	os.execute(cmd) 
end

function XExternalTextEditorPlugin:OnShortcut(edit, shortcut, source, ...)
	if shortcut == "Ctrl-E" then
		self:OpenEditor(edit)
		return true
	end
end

function XExternalTextEditorPlugin:OnTextChanged(edit)
	-- update external file
	if g_ExternalTextEditorActiveCtrl == edit then	
		local file_path = "AppData/editorplugin/" .. config.DefaultExternalTextEditorTempFile
		AsyncStringToFile(file_path, edit:GetText())
	end
end

function XExternalTextEditorPlugin.ApplyEdit(file, change)
	if change == "Modified" then
		local err, content = AsyncFileToString(file)
		if not err then
			if g_ExternalTextEditorActiveCtrl then
				g_ExternalTextEditorActiveCtrl:SetText(content)
			end
		end
	end
end


----- XHighlightTextPlugin

DefineClass.XHighlightTextPlugin = {
	__parents = { "XTextEditorPlugin" },
	HighlightColor = RGB(188, 168, 70),
	SingleInstance = false,
	
	highlighted_text = false,
	ignore_case = true,
}

local function find_next(str_lower, str, substr, start_pos)
	local idx = string.find(str_lower, substr, start_pos, true)
	if idx then
		return str:sub(start_pos, idx - 1), str:sub(idx, idx + #substr - 1)
	else
		return str:sub(start_pos)
	end
end

function XHighlightTextPlugin:OnAfterDrawText(edit, line_idx, text, target_box, font, text_color)
	if not self.highlighted_text or self.highlighted_text == "" then return end
	
	local pos = 1
	local text_start = target_box:minx()
	local lower_text = self.ignore_case and text:lower() or text
	local other, word
	repeat
		other, word = find_next(lower_text, text, self.highlighted_text, pos)
		if word then
			local len_word = utf8.len(word)
			local len_other = utf8.len(other)
			local x1 = MeasureToCharStart(text, font, pos + len_other)
			local x2 = MeasureToCharStart(text, font, pos + len_word + len_other)
			local word_box = box(text_start + x1, target_box:miny(), text_start + x2, target_box:maxy())
			StretchText(word, word_box, font, self.HighlightColor)
			pos = pos + len_word + len_other
		end
	until not word
	return true
end