-
Notifications
You must be signed in to change notification settings - Fork 1
/
editing.lua
453 lines (421 loc) · 14.8 KB
/
editing.lua
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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
--[[
Mitchell's editing.lua
Copyright (c) 2006-2008 Mitchell Foral. All rights reserved.
SciTE-tools homepage: http://caladbolg.net/scite.php
Send email to: mitchell<att>caladbolg<dott>net
Permission to use, copy, modify, and distribute this file
is granted, provided credit is given to Mitchell.
]]--
---
-- Editing commands for the scite module.
-- There are several option variables used:
-- PLATFORM: OS platform (linux or windows).
-- FILE_IN: Location of the temporary file used as STDIN for various
-- operations.
-- FILE_OUT: Location of the temporary file that will contain output for
-- various operations.
-- REDIRECT: The command line symbol used for redirecting STDOUT to a file.
-- RUBY_CMD: The command that executes the Ruby interpreter.
-- FMTP_CMD: (Linux only) The command used for reformatting paragraphs.
module('modules.scite.editing', package.seeall)
-- platform specific options
local PLATFORM = _G.PLATFORM or 'linux'
local FILE_IN, FILE_OUT, REDIRECT, RUBY_CMD, FMTP_CMD
if PLATFORM == 'linux' then
FILE_IN = '/tmp/scite_input'
FILE_OUT = '/tmp/scite_output'
REDIRECT = ' &> '
RUBY_CMD = 'ruby '
FMTP_CMD = 'fmt -c -w 80 '
elseif PLATFORM == 'windows' then
FILE_IN = os.getenv('TEMP')..'\\scite_input.rb'
FILE_OUT = os.getenv('TEMP')..'\\scite_output.txt'
REDIRECT = ' > '
RUBY_CMD = 'ruby '
end
-- end options
---
-- [Local table] The kill-ring.
-- @class table
-- @name kill_ring
-- @field maxn The maximum size of the kill-ring.
local kill_ring = { pos = 1, maxn = 10 }
---
-- [Local table] Character matching.
-- Matches parentheses, brackets, braces, and quotes.
-- @class table
-- @name char_matches
local char_matches = {
['('] = ')', ['['] = ']', ['{'] = '}',
["'"] = "'", ['"'] = '"'
}
---
-- [Local table] Enclosures for enclosing or selecting ranges of text.
-- Note chars and tag enclosures are generated at runtime.
-- @class table
-- @name enclosure
local enclosure = {
dbl_quotes = { left = '"', right = '"' },
sng_quotes = { left = "'", right = "'" },
parens = { left = '(', right = ')' },
brackets = { left = '[', right = ']' },
braces = { left = '{', right = '}' },
chars = { left = ' ', right = ' ' },
tags = { left = '>', right = '<' },
tag = { left = ' ', right = ' ' },
single_tag = { left = '<', right = ' />' }
}
---
-- SciTE Lua OnChar extension function.
-- Matches characters specified in char_matches if the editor pane has focus.
function _G.OnChar(c)
if char_matches[c] and editor.Focus then
editor:InsertText( -1, char_matches[c] )
end
end
-- local functions
local insert_into_kill_ring, scroll_kill_ring
local get_preceding_number, get_sel_or_line
---
-- Cuts or copies text ranges intelligently. (Behaves like Emacs.)
-- If no text is selected, all text from the cursor to the end of the line is
-- cut or copied as indicated by action and pushed onto the kill-ring. If there
-- is text selected, it is cut or copied and pushed onto the kill-ring.
-- @param action The action to perform. Cut is done by default. 'copy' copies
-- text instead.
-- @see insert_into_kill_ring
function smart_cutcopy(action)
local txt = editor:GetSelText()
if #txt == 0 then editor:LineEndExtend() end
txt = editor:GetSelText()
insert_into_kill_ring(txt)
kill_ring.pos = 1
if action ~= 'copy' then editor:Cut() return end
editor:Copy()
end
---
-- Retrieves the top item off the kill-ring and pastes it.
-- If an action is specified, the text is kept selected for scrolling through
-- the kill-ring.
-- @param action If given, specifies whether to cycle through the kill-ring in
-- normal or reverse order. A value of 'cycle' cycles through normally,
-- 'reverse' in reverse.
-- @see scroll_kill_ring
function smart_paste(action)
local anchor, pos = editor.Anchor, editor.CurrentPos
if pos < anchor then anchor = pos end
local txt = editor:GetSelText()
if txt == kill_ring[kill_ring.pos] then scroll_kill_ring(action) end
if scite.GetClipboardText then
-- If text was copied to the clipboard from other apps, insert it into the
-- kill-ring so it can be pasted (thanks to Nathan Robinson).
local clip_txt, found = scite.GetClipboardText(), false
if clip_txt ~= '' then
for _, ring_txt in ipairs(kill_ring) do
if clip_txt == ring_txt then found = true break end
end
end
if not found then insert_into_kill_ring(clip_txt) end
end
txt = kill_ring[kill_ring.pos]
if txt then
editor:ReplaceSel(txt)
if action then editor.Anchor = anchor end -- cycle
end
end
---
-- Selects the current word under the caret and if action indicates, delete it.
-- @param action Optional action to perform with selected word. If 'delete', it
-- is deleted.
function current_word(action)
local s = editor:WordStartPosition(editor.CurrentPos)
local e = editor:WordEndPosition(editor.CurrentPos)
editor:SetSel(s, e)
if action == 'delete' then editor:DeleteBack() end
end
---
-- Transposes characters intelligently.
-- If the carat is at the end of the current word, the two characters before
-- the caret are transposed. Otherwise the characters to the left and right of
-- the caret are transposed.
function transpose_chars()
editor:BeginUndoAction()
local pos = editor.CurrentPos
local char = editor.CharAt[pos - 1]
editor:DeleteBack()
if pos > editor.Length or editor.CharAt[pos - 1] == 32 then
editor:CharLeft()
else
editor:CharRight()
end
editor:InsertText( -1, string.char(char) )
editor:SetSel(pos, pos)
editor:EndUndoAction()
end
---
-- Reduces multiple characters occurances to just one.
-- If char is not given, the character to be squeezed is the one under the
-- caret.
-- @param char The character to be used for squeezing.
function squeeze(char)
if not char then char = editor.CharAt[editor.CurrentPos - 1] end
local s, e = editor.CurrentPos - 1, editor.CurrentPos - 1
while editor.CharAt[s] == char do s = s - 1 end
while editor.CharAt[e] == char do e = e + 1 end
editor:SetSel(s + 1, e)
editor:ReplaceSel( string.char(char) )
end
---
-- Joins the current line with the line below, eliminating whitespace.
function join_lines()
editor:BeginUndoAction()
editor:LineEnd() editor:Clear() editor:AddText(' ') squeeze()
editor:EndUndoAction()
end
---
-- Moves the current line in the specified direction up or down.
-- @param direction 'up' moves the current line up, 'down' moves it down.
function move_line(direction)
local column = editor.Column[editor.CurrentPos]
editor:BeginUndoAction()
if direction == 'up' then
editor:LineTranspose()
editor:LineUp()
elseif direction == 'down' then
editor:LineDown()
editor:LineTranspose()
column = editor.CurrentPos + column -- starts at line home
editor:SetSel(column, column)
end
editor:EndUndoAction()
end
---
-- Encloses text in an enclosure set.
-- If text is selected, it is enclosed. Otherwise, the previous word is
-- enclosed. The n previous words can be enclosed by appending n (a number) to
-- the end of the last word. When enclosing with a character, append the
-- character to the end of the word(s). To enclose previous word(s) with n
-- characters, append n (a number) to the end of character set.
-- Examples:
-- enclose this2 -> 'enclose this' (enclose in sng_quotes)
-- enclose this2**2 -> **enclose this**
-- @param str The enclosure type in enclosure.
-- @see enclosure
-- @see get_preceding_number
function enclose(str)
editor:BeginUndoAction()
local txt = editor:GetSelText()
if txt == '' then
if str == 'chars' then
local num_chars, len_num_chars = get_preceding_number()
for i = 1, len_num_chars do editor:DeleteBack() end
for i = 1, num_chars do editor:CharLeftExtend() end
enclosure[str].left = editor:GetSelText()
enclosure[str].right = enclosure[str].left
editor:DeleteBack()
end
local num_words, len_num_chars = get_preceding_number()
for i = 1, len_num_chars do editor:DeleteBack() end
for i = 1, num_words do editor:WordLeftExtend() end
txt = editor:GetSelText()
end
local len = 0
if str == 'tag' then
enclosure[str].left = '<'..txt..'>'
enclosure[str].right = '</'..txt..'>'
len = #txt + 3
txt = ''
end
local left = enclosure[str].left
local right = enclosure[str].right
editor:ReplaceSel(left..txt..right)
if str == 'tag' then editor:GotoPos(editor.CurrentPos - len) end
editor:EndUndoAction()
end
---
-- Selects text in a specified enclosure.
-- @param str The enclosure type in enclosure. If str is not specified,
-- matching character pairs defined in char_matches are searched for from the
-- caret outwards.
-- @see enclosure
-- @see char_matches
function select_enclosed(str)
if str then
editor:SearchAnchor(editor.CurrentPos)
local s = editor:SearchPrev( 0, enclosure[str].left )
local e = editor:SearchNext( 0, enclosure[str].right )
if s and e then editor:SetSel(s + 1, e) end
else
-- TODO: ignore enclosures in comment scopes?
s, e = editor.Anchor, editor.CurrentPos
if s > e then s, e = e, s end
local char = string.char( editor.CharAt[s - 1] )
if s ~= e and char_matches[char] then
s, e = s - 2, e + 1 -- don't match the same enclosure
end
while s >= 0 do
char = string.char( editor.CharAt[s] )
if char_matches[char] then
local _, e = editor:findtext( char_matches[char], 0, e )
if e then editor:SetSel(s + 1, e - 1) break end
end
s = s - 1
end
end
end
---
-- Selects the current line.
function select_line() editor:Home() editor:LineEndExtend() end
---
-- Selects the current paragraph.
-- Paragraphs are delimited by two consecutive newlines.
function select_paragraph() editor:ParaUp() editor:ParaDownExtend() end
---
-- Selects indented blocks intelligently.
-- If no block of text is selected, all text with the current level of
-- indentation is selected. If a block of text is selected and the lines to the
-- top and bottom of it are one indentation level lower, they are added to the
-- selection. In all other cases, the behavior is the same as if no text is
-- selected.
function select_indented_block()
local s = editor:LineFromPosition(editor.Anchor)
local e = editor:LineFromPosition(editor.CurrentPos)
if s > e then s, e = e, s end
local indent = editor.LineIndentation[s] - editor.Indent
if indent < 0 then return end
if editor:GetSelText() ~= '' then
if editor.LineIndentation[s - 1] == indent and
editor.LineIndentation[e + 1] == indent then
s, e = s - 1, e + 1
indent = indent + editor.Indent -- don't run while loops
end
end
while editor.LineIndentation[s - 1] > indent do s = s - 1 end
while editor.LineIndentation[e + 1] > indent do e = e + 1 end
s = editor:PositionFromLine(s)
e = editor.LineEndPosition[e]
editor:SetSel(s, e)
end
---
-- Selects all text with the same scope/style as under the caret.
function select_scope()
local start_pos = editor.CurrentPos
local base_style = editor.StyleAt[start_pos]
local pos = start_pos - 1
while editor.StyleAt[pos] == base_style do pos = pos - 1 end
local start_style = pos
pos = start_pos + 1
while editor.StyleAt[pos] == base_style do pos = pos + 1 end
editor:SetSel(start_style + 1, pos)
end
---
-- Executes the selection or contents of the current line as Ruby code,
-- replacing the text with the output.
function ruby_exec()
local txt = get_sel_or_line()
local f, out
-- write the file
f = io.open(FILE_IN, 'w') f:write(txt) f:close()
-- check the syntax
os.execute(RUBY_CMD..'-cw '..FILE_IN..REDIRECT..FILE_OUT)
f = io.open(FILE_OUT) out = f:read('*all') f:close()
-- execute the file
if out == 'Syntax OK\n' then
os.execute(RUBY_CMD..FILE_IN..REDIRECT..FILE_OUT)
f = io.open(FILE_OUT) out = f:read('*all') f:close()
if out:sub(-1) == '\n' then out = out:sub(1, -2) end
end
editor:ReplaceSel(out)
end
---
-- Executes the selection or contents of the current line as Lua code,
-- replacing the text with the output.
function lua_exec()
local txt = get_sel_or_line()
dostring(txt)
editor:SetSel(editor.CurrentPos, editor.CurrentPos)
end
---
-- Reformats the selected text or current paragraph using the command FMTP_CMD.
function reformat_paragraph()
if PLATFORM ~= 'linux' then print('Linux only') return end
if editor:GetSelText() == '' then select_paragraph() end
local txt = editor:GetSelText()
local f, out
f = io.open(FILE_IN, 'w') f:write(txt) f:close()
os.execute(FMTP_CMD..FILE_IN..REDIRECT..FILE_OUT)
f = io.open(FILE_OUT) out = f:read('*all') f:close()
if txt:sub(-1) ~= '\n' and out:sub(-1) == '\n' then out = out:sub(1, -2) end
editor:ReplaceSel(out)
end
--[[
---
-- Jumps to a buffer and location specified by a CTag selected or on the
-- current line.
function goto_ctag()
local line = get_sel_or_line()
local s1, s2, tag_name, file_name, tag_pattern =
string.find(line, '([^\t]*)\t([^\t]*)\t(.*)$')
if file_name == nil then return end
scite.Open(file_name)
s1 = string.find(tag_pattern, '$/')
if s1 ~= nil then
tag_pattern = string.sub(tag_pattern, 3, s1 - 1)
tag_pattern = string.gsub(tag_pattern, '\\/', '/')
local p1, p2 = editor:findtext(tag_pattern)
if p2 then editor:SetSel(p1, p2) end
else -- line numbers
s1 = string.find(tag_pattern, ';')
tag_pattern = string.sub(tag_pattern, 0, s1 - 1)
local tag_line = tonumber(tag_pattern) - 1
editor:GotoLine(tag_line)
end
local fline = editor.FirstVisibleLine
local cline = editor:LineFromPosition(editor.CurrentPos)
editor:LineScroll(0, cline - fline)
end
]]--
---
-- [Local function] Inserts text into kill_ring.
-- If it grows larger than maxn, the oldest inserted text is replaced.
-- @see smart_cutcopy
insert_into_kill_ring = function(txt)
table.insert(kill_ring, 1, txt)
local maxn = kill_ring.maxn
if #kill_ring > maxn then kill_ring[maxn + 1] = nil end
end
---
-- [Local function] Scrolls kill_ring in the specified direction.
-- @param direction The direction to scroll: 'forward' (default) or 'reverse'.
-- @see smart_paste
scroll_kill_ring = function(direction)
if direction == 'reverse' then
kill_ring.pos = kill_ring.pos - 1
if kill_ring.pos < 1 then kill_ring.pos = #kill_ring end
else
kill_ring.pos = kill_ring.pos + 1
if kill_ring.pos > #kill_ring then kill_ring.pos = 1 end
end
end
---
-- [Local function] Returns the number to the left of the caret.
-- This is used for the enclose function.
-- @see enclose
get_preceding_number = function()
local pos = editor.CurrentPos
local char = editor.CharAt[pos - 1]
local txt = ''
while tonumber( string.char(char) ) do
txt = txt..string.char(char)
pos = pos - 1
char = editor.CharAt[pos - 1]
end
return tonumber(txt) or 1, #txt
end
---
-- [Local function] Returns the current selection or the contents of the
-- current line.
get_sel_or_line = function()
if editor:GetSelText() == '' then select_line() end
return editor:GetSelText()
end