diff --git a/LICENSE-APACHE-2.0 b/LICENSE-APACHE-2.0 new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE-APACHE-2.0 @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/config/lua/ivy/treesitter-indentexpr.lua b/config/lua/ivy/treesitter-indentexpr.lua new file mode 100644 index 0000000..6f6afe3 --- /dev/null +++ b/config/lua/ivy/treesitter-indentexpr.lua @@ -0,0 +1,342 @@ +-- copied from @nvim-treesitter https://github.com/nvim-treesitter/nvim-treesitter/blob/6620ae1c44dfa8623b22d0cbf873a9e8d073b849/lua/nvim-treesitter/indent.lua +-- license: APACHE-2.0 + +local ts = vim.treesitter + +local M = {} + +M.comment_parsers = { + comment = true, + luadoc = true, + javadoc = true, + jsdoc = true, + phpdoc = true, +} + +local function getline(lnum) + return vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, false)[1] or "" +end + +---@param lnum integer +---@return integer +local function get_indentcols_at_line(lnum) + local _, indentcols = getline(lnum):find("^%s*") + return indentcols or 0 +end + +---@param root TSNode +---@param lnum integer +---@param col? integer +---@return TSNode? +local function get_first_node_at_line(root, lnum, col) + col = col or get_indentcols_at_line(lnum) + return root:descendant_for_range(lnum - 1, col, lnum - 1, col + 1) +end + +---@param root TSNode +---@param lnum integer +---@param col? integer +---@return TSNode? +local function get_last_node_at_line(root, lnum, col) + col = col or (#getline(lnum) - 1) + return root:descendant_for_range(lnum - 1, col, lnum - 1, col + 1) +end + +---@param bufnr integer +---@param node TSNode +---@param delimiter string +---@return TSNode? child +---@return boolean? is_end +local function find_delimiter(bufnr, node, delimiter) + for child, _ in node:iter_children() do + if child:type() == delimiter then + local linenr = child:start() + local line = vim.api.nvim_buf_get_lines(bufnr, linenr, linenr + 1, false)[1] + local end_char = { child:end_() } + local trimmed_after_delim + local escaped_delimiter = delimiter:gsub("[%-%.%+%[%]%(%)%$%^%%%?%*]", "%%%1") + trimmed_after_delim = assert(line):sub(end_char[2] + 1):gsub("[%s" .. escaped_delimiter .. "]*", "") + return child, #trimmed_after_delim == 0 + end + end +end + +---Memoize a function using hash_fn to hash the arguments. +---@generic F: function +---@param fn F +---@param hash_fn fun(...): any +---@return F +local function memoize(fn, hash_fn) + local cache = setmetatable({}, { __mode = "kv" }) ---@type table + + return function(...) + local key = hash_fn(...) + if cache[key] == nil then + local v = fn(...) ---@type any + cache[key] = v ~= nil and v or vim.NIL + end + + local v = cache[key] + return v ~= vim.NIL and v or nil + end +end + +local get_indents = memoize(function(bufnr, root, lang) + ---@type table> + local map = { + ["indent.auto"] = {}, + ["indent.begin"] = {}, + ["indent.end"] = {}, + ["indent.dedent"] = {}, + ["indent.branch"] = {}, + ["indent.ignore"] = {}, + ["indent.align"] = {}, + ["indent.zero"] = {}, + } + + local query = ts.query.get(lang, "indents") + if not query then + return map + end + for id, node, metadata in query:iter_captures(root, bufnr) do + if query.captures[id]:sub(1, 1) ~= "_" then + map[query.captures[id]][node:id()] = metadata or {} + end + end + + return map +end, function(bufnr, root, lang) + return tostring(bufnr) .. root:id() .. "_" .. lang +end) + +---@param lnum integer (1-indexed) +---@return integer +function M.get_indent(lnum) + local bufnr = vim.api.nvim_get_current_buf() + local parser = ts.get_parser(bufnr) + if not parser or not lnum then + return -1 + end + + parser:parse({ vim.fn.line("w0") - 1, vim.fn.line("w$") }) + + -- Get language tree with smallest range around node that's not a comment parser + local root, lang_tree ---@type TSNode, vim.treesitter.LanguageTree + parser:for_each_tree(function(tstree, tree) + if not tstree or M.comment_parsers[tree:lang()] then + return + end + local local_root = tstree:root() + if ts.is_in_node_range(local_root, lnum - 1, 0) then + if not root or root:byte_length() >= local_root:byte_length() then + root = local_root + lang_tree = tree + end + end + end) + + -- Not likely, but just in case... + if not root then + return 0 + end + + local q = get_indents(vim.api.nvim_get_current_buf(), root, lang_tree:lang()) + local node ---@type TSNode? + if getline(lnum):find("^%s*$") then + local prevlnum = vim.fn.prevnonblank(lnum) + local indentcols = get_indentcols_at_line(prevlnum) + local prevline = vim.trim(getline(prevlnum)) + -- The final position can be trailing spaces, which should not affect indentation + node = get_last_node_at_line(root, prevlnum, indentcols + #prevline - 1) + if node and node:type():match("comment") then + -- The final node we capture of the previous line can be a comment node, which should also be ignored + -- Unless the last line is an entire line of comment, ignore the comment range and find the last node again + local first_node = get_first_node_at_line(root, prevlnum, indentcols) + local _, scol, _, _ = node:range() + if first_node and first_node:id() ~= node:id() then + -- In case the last captured node is a trailing comment node, re-trim the string + prevline = vim.trim(prevline:sub(1, scol - indentcols)) + -- Add back indent as indent of prevline was trimmed away + local col = indentcols + #prevline - 1 + node = get_last_node_at_line(root, prevlnum, col) + end + end + if node and q["indent.end"][node:id()] then + node = get_first_node_at_line(root, lnum) + end + else + node = get_first_node_at_line(root, lnum) + end + + local indent_size = vim.fn.shiftwidth() + local indent = 0 + local _, _, root_start = root:start() + if root_start ~= 0 then + -- injected tree + indent = vim.fn.indent(root:start() + 1) + end + + -- tracks to ensure multiple indent levels are not applied for same line + local is_processed_by_row = {} --- @type table + + if node and q["indent.zero"][node:id()] then + return 0 + end + + while node do + -- do 'autoindent' if not marked as @indent + if + not q["indent.begin"][node:id()] + and not q["indent.align"][node:id()] + and q["indent.auto"][node:id()] + and node:start() < lnum - 1 + and lnum - 1 <= node:end_() + then + return -1 + end + + -- Do not indent if we are inside an @ignore block. + -- If a node spans from L1,C1 to L2,C2, we know that lines where L1 < line <= L2 would + -- have their indentations contained by the node. + if + not q["indent.begin"][node:id()] + and q["indent.ignore"][node:id()] + and node:start() < lnum - 1 + and lnum - 1 <= node:end_() + then + return 0 + end + + local srow, _, erow = node:range() + + local is_processed = false + + if + not is_processed_by_row[srow] + and ((q["indent.branch"][node:id()] and srow == lnum - 1) or (q["indent.dedent"][node:id()] and srow ~= lnum - 1)) + then + indent = indent - indent_size + is_processed = true + end + + -- do not indent for nodes that starts-and-ends on same line and starts on target line (lnum) + local should_process = not is_processed_by_row[srow] + local is_in_err = false + if should_process then + local parent = node:parent() + is_in_err = parent and parent:has_error() or false + end + if + should_process + and ( + q["indent.begin"][node:id()] + and (srow ~= erow or is_in_err or q["indent.begin"][node:id()]["indent.immediate"]) + and (srow ~= lnum - 1 or q["indent.begin"][node:id()]["indent.start_at_same_line"]) + ) + then + indent = indent + indent_size + is_processed = true + end + + if is_in_err and not q["indent.align"][node:id()] then + -- only when the node is in error, promote the + -- first child's aligned indent to the error node + -- to work around ((ERROR "X" . (_)) @aligned_indent (#set! "delimiter" "AB")) + -- matching for all X, instead set do + -- (ERROR "X" @aligned_indent (#set! "delimiter" "AB") . (_)) + -- and we will fish it out here. + for c in node:iter_children() do + if q["indent.align"][c:id()] then + q["indent.align"][node:id()] = q["indent.align"][c:id()] + break + end + end + end + -- do not indent for nodes that starts-and-ends on same line and starts on target line (lnum) + if should_process and q["indent.align"][node:id()] and (srow ~= erow or is_in_err) and (srow ~= lnum - 1) then + local metadata = q["indent.align"][node:id()] + local o_delim_node, o_is_last_in_line ---@type TSNode?, boolean? + local c_delim_node, c_is_last_in_line ---@type TSNode?, boolean?, boolean? + local indent_is_absolute = false + if metadata["indent.open_delimiter"] then + o_delim_node, o_is_last_in_line = find_delimiter(bufnr, node, metadata["indent.open_delimiter"]) + else + o_delim_node = node + end + if metadata["indent.close_delimiter"] then + c_delim_node, c_is_last_in_line = find_delimiter(bufnr, node, metadata["indent.close_delimiter"]) + else + c_delim_node = node + end + + if o_delim_node then + local o_srow, o_scol = o_delim_node:start() + local c_srow = nil --- @type integer? + if c_delim_node then + c_srow = c_delim_node:start() + end + if o_is_last_in_line then + -- hanging indent (previous line ended with starting delimiter) + -- should be processed like indent + if should_process then + indent = indent + indent_size * 1 + if c_is_last_in_line then + -- If current line is outside the range of a node marked with `@aligned_indent` + -- Then its indent level shouldn't be affected by `@aligned_indent` node + if c_srow and c_srow < lnum - 1 then + indent = math.max(indent - indent_size, 0) + end + end + end + else + -- aligned indent + if c_is_last_in_line and c_srow and o_srow ~= c_srow and c_srow < lnum - 1 then + -- If current line is outside the range of a node marked with `@aligned_indent` + -- Then its indent level shouldn't be affected by `@aligned_indent` node + indent = math.max(indent - indent_size, 0) + else + indent = o_scol + (metadata["indent.increment"] or 1) + indent_is_absolute = true + end + end + -- deal with the final line + local avoid_last_matching_next = false + if c_srow and c_srow ~= o_srow and c_srow == lnum - 1 then + -- delims end on current line, and are not open and closed same line. + -- then this last line may need additional indent to avoid clashes + -- with the next. `indent.avoid_last_matching_next` controls this behavior, + -- for example this is needed for function parameters. + avoid_last_matching_next = metadata["indent.avoid_last_matching_next"] or false + end + if avoid_last_matching_next then + -- last line must be indented more in cases where + -- it would be same indent as next line (we determine this as one + -- width more than the open indent to avoid confusing with any + -- hanging indents) + if indent <= vim.fn.indent(o_srow + 1) + indent_size then + indent = indent + indent_size * 1 + else + indent = indent + end + end + is_processed = true + if indent_is_absolute then + -- don't allow further indenting by parent nodes, this is an absolute position + return indent + end + end + end + + is_processed_by_row[srow] = is_processed_by_row[srow] or is_processed + + node = node:parent() + end + + return indent +end + +function M.indent() + return M.get_indent(vim.v.lnum) +end + +return M diff --git a/config/plugin/treesitter.lua b/config/plugin/treesitter.lua index f27a59d..134858e 100644 --- a/config/plugin/treesitter.lua +++ b/config/plugin/treesitter.lua @@ -37,6 +37,7 @@ vim.on("FileType", "*", {}, function(ev) if not ok then return end - vim.o.foldmethod = "expr" - vim.o.foldexpr = "v:lua.vim.treesitter.foldexpr()" + vim.wo.foldmethod = "expr" + vim.wo.foldexpr = "v:lua.vim.treesitter.foldexpr()" + vim.bo.indentexpr = "v:lua.require('ivy.treesitter-indentexpr').indent()" end)