forked from orbitalquark/textadept-format
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinit.lua
More file actions
198 lines (182 loc) · 7.11 KB
/
init.lua
File metadata and controls
198 lines (182 loc) · 7.11 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
197
198
-- 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 = {}
--- Helper function that returns whether or not the given config file exists in the current or
-- a parent directory of the current buffer's filename.
local function has_config_file(filename, contains)
if not buffer.filename then return false end
local dir = buffer.filename:match('^(.+)[/\\]')
while dir do
if lfs.attributes(dir .. '/' .. filename) then
if not contains then return true
else
local f = io.open(dir .. '/' .. filename, mode)
if f then
local found = f:read('a'):match(contains)
f:close()
if found then return true end
end
end
end
dir = dir:match('^(.+)[/\\]')
end
return false
end
local function get_prettier_parser()
-- Most parsers are the same as the lexer name
local parser = buffer:get_lexer()
if parser == 'javascript' then
parser = 'babel'
end
return has_config_file('package.json', 'prettier') and 'npx prettier --parser ' .. parser or nil
end
--- Map of lexer languages to string code formatter commands or functions that return such
-- commands.
M.commands = {
lua = function() return has_config_file('.lua-format') and 'lua-format' or nil end,
cpp = function() return has_config_file('.clang-format') and 'clang-format -style=file' or nil end,
html = get_prettier_parser, css = get_prettier_parser, javascript = get_prettier_parser,
markdown = get_prettier_parser, yaml = get_prettier_parser,
go = 'gofmt', dart = 'dart format',
python = function ()
if has_config_file('ruff.toml') or has_config_file('pyproject.toml', 'ruff') then
return 'ruff format -'
else
return ((WIN32 and 'py -m ' or '') .. 'black -')
end
end
}
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
textadept.editing.filter_through(command)
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 = (not OSX 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