-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathinit.lua
More file actions
196 lines (180 loc) · 7.41 KB
/
init.lua
File metadata and controls
196 lines (180 loc) · 7.41 KB
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
-- Copyright 2021-2026 Mitchell. See LICENSE.
--- Format/reformat paragraph and code.
-- Install this module by copying it into your *~/.textadept/modules/* directory or Textadept's
-- *modules/* directory, and then putting the following in your *~/.textadept/init.lua*:
--
-- ```lua
-- local format = require('format')
-- ```
--
-- There will be an "Edit > Reformat" menu.
--
-- ## Key Bindings
--
-- Windows and Linux | macOS | Terminal | Command
-- -|-|-|-
-- **Edit**| | |
-- Ctrl+Shift+J | ⌘⇧J | M-S-J | Reformat paragraph
-- @module format
local M = {}
--- Returns whether or not the given config file exists in the current or a parent directory of
-- the current buffer's filename.
-- Also returns the config file's filename.
-- @param filename String config filename.
-- @usage format.config_file_exists('ruff.toml')
function M.config_file_exists(filename)
if not buffer.filename then return false end
local dir = buffer.filename:match('^(.+)[/\\]')
while dir do
local config_file = dir .. '/' .. filename
if lfs.attributes(config_file) then return true, config_file end
dir = dir:match('^(.+)[/\\]')
end
return false, nil
end
--- Returns whether or not the given config file exists in the current or a parent directory of
-- the current buffer's filename, and whether or not it contains the given text.
-- @param filename String config filename.
-- @param text String text to look for.
-- @usage format.config_file_contains('pyproject.toml', '[tool.ruff')
function M.config_file_contains(filename, text)
local exists, config_file = M.config_file_exists(filename)
if not exists then return false end
local f<close> = io.open(config_file)
return f:read('a'):find(text, 1, true) ~= nil
end
--- Map of lexer languages to string code formatter commands or functions that return such
-- commands.
-- Commands should accept code via stdin and output formatted code to stdout.
-- @usage format.commands.python = 'black -'
M.commands = {
lua = function() return M.config_file_exists('.lua-format') and 'lua-format' or nil end,
cpp = function()
return M.config_file_exists('.clang-format') and 'clang-format -style=file' or nil
end, --
python = function()
if M.config_file_exists('ruff.toml') or M.config_file_contains('pyproject.toml', '[tool.ruff') then
return 'ruff format -'
end
end, --
go = 'gofmt', dart = 'dart format'
}
M.commands.c = M.commands.cpp
--- List of header lines to ignore when reformatting paragraphs.
-- These can be LuaDoc/LDoc or Doxygen headers for example.
-- @usage table.insert(format.ignore_header_lines, '"""')
M.ignore_header_lines = {'---', '/**'}
--- Prefixes to remap when reformatting paragraphs.
-- This is for paragraphs that have a first-line prefix that is different from subsequent
-- line prefixes. For example, LuaDoc/LDoc comments start with '---' but continue with '--',
-- and Doxygen comments start with '/**' but continue with ' *'.
-- @usage format.prefix_map['##'] = '#'
M.prefix_map = {['/**'] = ' *', ['---'] = '--'}
--- List of footer lines to ignore when reformatting paragraphs.
-- These can be Doxygen footers for example.
-- @usage table.insert(format.ignore_footer_lines, '"""')
M.ignore_footer_lines = {'*/'}
--- List of Lua patterns that match filenames to ignore when formatting on save.
-- This is useful for projects with a top-level format config file, but subfolder dependencies
-- whose code should not be formatted on save.
-- @usage table.insert(format.ignore_file_patterns, '/testdata/')
M.ignore_file_patterns = {}
--- Invoke a code formatter on save.
-- The default value is `true`.
M.on_save = true
--- The maximum number of characters to allow on a line when reformatting paragraphs. The default
-- value is 100.
M.line_length = 100
--- Reformats using a code formatter for the current buffer's lexer language either the selected
-- text or the current buffer, according to the rules of `textadept.editing.filter_through()`.
-- @see commands
function M.code()
local command = M.commands[buffer.lexer_language]
if type(command) == 'function' then command = command() end
if not command then return end
local current_dir = lfs.currentdir()
local dir = (buffer.filename or ''):match('^(.+)[/\\]') or io.get_project_root()
if dir and dir ~= current_dir then lfs.chdir(dir) end
local ok, errmsg = pcall(textadept.editing.filter_through, command)
if not ok then ui.statusbar_text = _L['Format error:'] .. ' ' .. errmsg:match('^.-: (.+)$') end
if dir and dir ~= current_dir then lfs.chdir(current_dir) end -- restore
end
events.connect(events.FILE_BEFORE_SAVE, function(filename)
if not M.on_save then return end
if filename then
for _, patt in ipairs(M.ignore_file_patterns) do if filename:find(patt) then return end end
end
local selection = buffer.selection_serialized
buffer:set_empty_selection(buffer.current_pos)
M.code()
buffer.selection_serialized = selection
end)
--- Reformats using the Unix `fmt` tool either the selected text or the current paragraph,
-- according to the rules of `textadept.editing.filter_through()`.
-- For styled text, paragraphs are either blocks of same-styled lines (e.g. code comments),
-- or lines surrounded by blank lines.
-- If the first line matches any of the lines in `format.ignore_header_lines`, it is not
-- reformatted. If the last line matches any of the lines in `format.ignore_footer_lines`,
-- it is not reformatted.
-- @see line_length
function M.paragraph()
if buffer.selection_empty then
local s = buffer:line_from_position(buffer.current_pos)
local style = buffer.style_at[buffer.line_indent_position[s]]
local e = s + 1
for i = s - 1, 1, -1 do
if buffer.style_at[buffer.line_indent_position[i]] ~= style then break end
s = s - 1
end
local line = buffer:get_line(s)
for _, header in ipairs(M.ignore_header_lines) do
if line:find('^%s*' .. header:gsub('%p', '%%%0') .. '%s*$') then
s = s + 1
break
end
end
for i = e, buffer.line_count do
if buffer.style_at[buffer.line_indent_position[i]] ~= style then break end
e = e + 1
end
line = buffer:get_line(e - 1)
for _, footer in ipairs(M.ignore_footer_lines) do
if line:find('^%s*' .. footer:gsub('%p', '%%%0')) then
e = e - 1
break
end
end
buffer:set_sel(buffer:position_from_line(s), buffer:position_from_line(e))
end
buffer:begin_undo_action()
local line_num = buffer:line_from_position(buffer.selection_start)
local prefix = buffer:get_line(line_num):match('^%s*(%p*)')
if M.prefix_map[prefix] then
-- Replace the prefix with its mapped prefix.
local pos = buffer:position_from_line(line_num)
buffer:set_target_range(pos, pos + #prefix)
buffer:replace_target(M.prefix_map[prefix])
end
local cmd = (OS ~= 'macos' and 'fmt' or 'gfmt') .. ' -w ' .. M.line_length .. ' -c'
if prefix ~= '' then cmd = string.format('%s -p "%s"', cmd, M.prefix_map[prefix] or prefix) end
textadept.editing.filter_through(cmd)
if M.prefix_map[prefix] then
-- Replace the mapped prefix with its original prefix.
buffer:set_target_range(buffer.selection_start, buffer.selection_start + #M.prefix_map[prefix])
buffer:replace_target(prefix)
buffer.selection_start = buffer.selection_start - #prefix
end
buffer:end_undo_action()
end
-- Add sub-menu.
_L['Reformat'] = 'Reformat'
_L['Code'] = '_Code'
_L['Paragraph'] = '_Paragraph'
local m_edit = textadept.menu.menubar['Edit']
table.insert(m_edit, #m_edit - 1, {
title = _L['Reformat'], --
{_L['Code'], M.code}, --
{_L['Paragraph'], M.paragraph}
})
keys.assign_platform_bindings{[M.paragraph] = {'ctrl+J', 'cmd+J', 'meta+J'}}
return M