|
| 1 | +local Server = require("formatls.server") |
| 2 | +local Proxy = require("formatls.proxy") |
| 3 | +local pipeline = require("formatls.pipeline") |
| 4 | + |
| 5 | +local function exec_action(bufnr, action_kind) |
| 6 | + local params = { |
| 7 | + textDocument = { uri = vim.uri_from_bufnr(bufnr) }, |
| 8 | + range = { |
| 9 | + start = { line = 0, character = 0 }, |
| 10 | + ["end"] = { line = vim.api.nvim_buf_line_count(bufnr), character = 0 }, |
| 11 | + }, |
| 12 | + context = { only = { action_kind }, diagnostics = {} }, |
| 13 | + } |
| 14 | + |
| 15 | + local clients = vim.lsp.get_clients({ bufnr = bufnr, method = "textDocument/codeAction" }) |
| 16 | + |
| 17 | + for _, client in ipairs(clients) do |
| 18 | + local res = client:request_sync("textDocument/codeAction", params, 5000, bufnr) |
| 19 | + if res and not res.err then |
| 20 | + for _, action in ipairs(res.result or {}) do |
| 21 | + if not action.edit then |
| 22 | + local resolved = client:request_sync("codeAction/resolve", action, 5000, bufnr) |
| 23 | + if resolved and resolved.result then |
| 24 | + action = resolved.result |
| 25 | + end |
| 26 | + end |
| 27 | + |
| 28 | + if action.edit then |
| 29 | + vim.lsp.util.apply_workspace_edit(action.edit, "utf-16") |
| 30 | + end |
| 31 | + if action.command then |
| 32 | + client:request_sync("workspace/executeCommand", action.command, 5000, bufnr) |
| 33 | + end |
| 34 | + end |
| 35 | + end |
| 36 | + end |
| 37 | +end |
| 38 | + |
| 39 | +local function get_lsp_edits(proxy, bufnr, method, params) |
| 40 | + for _, entry in ipairs(proxy:get_formatters(bufnr)) do |
| 41 | + local client = vim.lsp.get_client_by_id(entry.client_id) |
| 42 | + if client then |
| 43 | + local result = client:request_sync(method, params, 5000, bufnr) |
| 44 | + if result and result.result and #result.result > 0 then |
| 45 | + return result.result |
| 46 | + end |
| 47 | + end |
| 48 | + end |
| 49 | + return nil |
| 50 | +end |
| 51 | + |
| 52 | +local function handle_format(self, method, params) |
| 53 | + local bufnr = vim.uri_to_bufnr(params.textDocument.uri) |
| 54 | + local filepath = vim.uri_to_fname(params.textDocument.uri) |
| 55 | + local dirname = vim.fn.fnamemodify(filepath, ":h") |
| 56 | + |
| 57 | + local steps = pipeline.resolve_group(self.formatters_by_ft[vim.bo[bufnr].filetype] or {}, dirname) |
| 58 | + |
| 59 | + local lsp_edits = function() |
| 60 | + return get_lsp_edits(self.proxy, bufnr, method, params) |
| 61 | + end |
| 62 | + |
| 63 | + if not steps then |
| 64 | + return lsp_edits() or {} |
| 65 | + end |
| 66 | + |
| 67 | + local original = table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n") .. "\n" |
| 68 | + |
| 69 | + local final = pipeline.run_pipeline(steps, { |
| 70 | + filepath = filepath, |
| 71 | + dirname = dirname, |
| 72 | + get_lsp_edits = lsp_edits, |
| 73 | + exec_action = function(action_kind, content) |
| 74 | + local lines = vim.split(content, "\n", { plain = true }) |
| 75 | + if #lines > 0 and lines[#lines] == "" then |
| 76 | + table.remove(lines) |
| 77 | + end |
| 78 | + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) |
| 79 | + exec_action(bufnr, action_kind) |
| 80 | + return table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n") .. "\n" |
| 81 | + end, |
| 82 | + }, original) |
| 83 | + |
| 84 | + if not final then |
| 85 | + -- Rollback buffer to original on pipeline failure |
| 86 | + local orig_lines = vim.split(original, "\n", { plain = true }) |
| 87 | + if #orig_lines > 0 and orig_lines[#orig_lines] == "" then |
| 88 | + table.remove(orig_lines) |
| 89 | + end |
| 90 | + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, orig_lines) |
| 91 | + return {} |
| 92 | + end |
| 93 | + |
| 94 | + -- Compute edits from the buffer's current state (which includes any action changes) |
| 95 | + local current = table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n") .. "\n" |
| 96 | + if final == current then |
| 97 | + return {} |
| 98 | + end |
| 99 | + |
| 100 | + return pipeline.compute_edits(current, final) |
| 101 | +end |
| 102 | + |
| 103 | +local M = Server.new("formatls") |
| 104 | + |
| 105 | +M.capabilities = { |
| 106 | + documentFormattingProvider = true, |
| 107 | + documentRangeFormattingProvider = true, |
| 108 | + textDocumentSync = { openClose = true }, |
| 109 | +} |
| 110 | + |
| 111 | +function M:on_init(params) |
| 112 | + local opts = params.initializationOptions or {} |
| 113 | + self.formatters_by_ft = opts.formatters_by_ft or {} |
| 114 | + self.notified_fts = {} |
| 115 | + self.proxy = Proxy.start(self.name) |
| 116 | + |
| 117 | + for name, spec in pairs(opts.formatters or {}) do |
| 118 | + pipeline.add_spec(name, spec) |
| 119 | + end |
| 120 | +end |
| 121 | + |
| 122 | +function M:on_shutdown() |
| 123 | + self.proxy:stop() |
| 124 | +end |
| 125 | + |
| 126 | +M.notifications["textDocument/didOpen"] = function(self, params) |
| 127 | + local bufnr = vim.uri_to_bufnr(params.textDocument.uri) |
| 128 | + local ft = vim.bo[bufnr].filetype |
| 129 | + |
| 130 | + if self.notified_fts[ft] then |
| 131 | + return |
| 132 | + end |
| 133 | + self.notified_fts[ft] = true |
| 134 | + |
| 135 | + local dirname = vim.fn.fnamemodify(vim.uri_to_fname(params.textDocument.uri), ":h") |
| 136 | + local steps = pipeline.resolve_group(self.formatters_by_ft[ft] or {}, dirname) |
| 137 | + if not steps then |
| 138 | + return |
| 139 | + end |
| 140 | + |
| 141 | + local names = {} |
| 142 | + for _, step in ipairs(steps) do |
| 143 | + names[#names + 1] = step.action or step.cmd or "lsp" |
| 144 | + end |
| 145 | + |
| 146 | + local title = table.concat(names, " | ") |
| 147 | + self.dispatchers.server_request("window/workDoneProgress/create", { token = self.name }, function() end) |
| 148 | + self.dispatchers.notification("$/progress", { token = self.name, value = { kind = "begin", title = title } }) |
| 149 | + self.dispatchers.notification("$/progress", { token = self.name, value = { kind = "end" } }) |
| 150 | +end |
| 151 | + |
| 152 | +M.requests["textDocument/formatting"] = function(self, params) |
| 153 | + return handle_format(self, "textDocument/formatting", params) |
| 154 | +end |
| 155 | + |
| 156 | +M.requests["textDocument/rangeFormatting"] = function(self, params) |
| 157 | + return handle_format(self, "textDocument/rangeFormatting", params) |
| 158 | +end |
| 159 | + |
| 160 | +return M:build() |
0 commit comments