diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40230f5b15..c686bb2a78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,6 @@ jobs: ${{ matrix.install-rg }} rg --version - git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ../plenary.nvim git clone --depth 1 https://github.com/nvim-tree/nvim-web-devicons ../nvim-web-devicons - name: Run tests diff --git a/.github/workflows/docgen.yml b/.github/workflows/docgen.yml index 9fe6a0a0a7..fbcd8e4c62 100644 --- a/.github/workflows/docgen.yml +++ b/.github/workflows/docgen.yml @@ -31,5 +31,5 @@ jobs: export PATH="${PWD}/_neovim/bin:${PATH}" export VIM="${PWD}/_neovim/share/nvim/runtime" nvim --version - make docgen + make docs git diff --exit-code diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 54d7bf9bf3..d54148a34e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,8 +20,6 @@ jobs: A highly extendable fuzzy finder over lists. Built on the latest awesome features from neovim core. Telescope is centered around modularity, allowing for easy customization. - dependencies: | - plenary.nvim copy_directories: | doc ftplugin diff --git a/Makefile b/Makefile index 3e4b4b4a8d..97b5e8f9d6 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,32 @@ -.PHONY: test lint docgen +DEPDIR ?= .deps +PLENTEST := $(DEPDIR)/plentest.nvim +DOCGEN := $(DEPDIR)/docgen.nvim +DOCGEN_TAG := v1.0.1 + +.PHONY: plentest +plentest: $(PLENTEST) + +$(PLENTEST): + git clone --filter=blob:none https://github.com/nvim-treesitter/plentest.nvim $(PLENTEST) -test: - nvim --headless --noplugin -u scripts/minimal_init.vim -c "PlenaryBustedDirectory lua/tests/automated/ { minimal_init = './scripts/minimal_init.vim' }" +.PHONY: test lint docgen +test: $(PLENTEST) + PLENTEST=$(PLENTEST) nvim --headless --clean -u scripts/minimal_init.lua \ + -c "lua require('plentest').test_directory('tests/automated', { minimal_init = './scripts/minimal_init.lua' })" lint: luacheck lua/telescope -.deps/docgen.nvim: - git clone --depth 1 --branch v1.0.1 https://github.com/jamestrew/docgen.nvim $@ +.PHONY: docgen +docgen: $(DOCGEN) + +$(DOCGEN): + git clone --filter=blob:none --branch $(DOCGEN_TAG) https://github.com/jamestrew/docgen.nvim $(DOCGEN) -docgen: .deps/docgen.nvim +.PHONY: docs +docs: $(DOCGEN) nvim -l scripts/gendocs.lua + +.PHONY: clean +clean: + rm -rf $(DEPDIR) diff --git a/README.md b/README.md index 86924f3145..005376bf84 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,6 @@ This section should guide you to run your first builtin pickers. ### Requirements * [Neovim >=v0.11.7](https://github.com/neovim/neovim/releases/tag/v0.11.6) built **with LuaJIT** (check `:version`). -* [nvim-lua/plenary.nvim](https://github.com/nvim-lua/plenary.nvim). >[!IMPORTANT] > Only the **latest version** ([`stable`](https://github.com/neovim/neovim/releases/tag/stable)) and the **latest commit of `HEAD`** ([`nightly`](https://github.com/neovim/neovim/releases/tag/nightly)) are tested and supported; before opening an issue, download the latest available release and confirm that the problem persists. @@ -74,9 +73,8 @@ e.g. using [lazy.nvim](https://github.com/folke/lazy.nvim) ```lua { 'nvim-telescope/telescope.nvim', version = '*', + -- optional but recommended dependencies = { - 'nvim-lua/plenary.nvim', - -- optional but recommended { 'nvim-telescope/telescope-fzf-native.nvim', build = 'make' }, } } diff --git a/doc/telescope.txt b/doc/telescope.txt index 47410ee8ba..afce4e4184 100644 --- a/doc/telescope.txt +++ b/doc/telescope.txt @@ -544,7 +544,7 @@ telescope.setup({opts}) *telescope.setup()* end, -- 2) Truncate lines to preview window for too large files filesize_hook = function(filepath, bufnr, opts) - local path = require("plenary.path"):new(filepath) + local path = require("neoplen.path"):new(filepath) -- opts exposes winid local height = vim.api.nvim_win_get_height(opts.winid) local lines = vim.split(path:head(height), "[\r]?\n") @@ -2454,8 +2454,7 @@ do the following: >lua Another interesting thing to do is that these actions now have functions you can call. These functions include `:replace(f)`, `:replace_if(f, c)`, `replace_map(tbl)` and `enhance(tbl)`. More information on these functions can -be found in the `developers.md` and `lua/tests/automated/action_spec.lua` -file. +be found in the `developers.md` and `tests/automated/action_spec.lua` file. *telescope.actions.move_selection_next()* actions.move_selection_next({prompt_bufnr}) diff --git a/doc/telescope_changelog.txt b/doc/telescope_changelog.txt index 08750a7b38..4a63ae09c8 100644 --- a/doc/telescope_changelog.txt +++ b/doc/telescope_changelog.txt @@ -98,26 +98,26 @@ telescope.path module soon. Guide to switch over to plenary.path - separator before: require("telescope.path").separator - now: require("plenary.path").path.sep + now: require("neoplen.path").pathsep - home before: require("telescope.path").home - now: require("plenary.path").path.home + now: require("neoplen.path").path.home - make_relative before: require("telescope.path").make_relative(filepath, cwd) - now: require("plenary.path"):new(filepath):make_relative(cwd) + now: require("neoplen.path"):new(filepath):make_relative(cwd) - shorten before: require("telescope.path").shorten(filepath) - now: require("plenary.path"):new(filepath):shorten() + now: require("neoplen.path"):new(filepath):shorten() with optional len, default is 1 - normalize before: require("telescope.path").normalize(filepath, cwd) - now: require("plenary.path"):new(filepath):normalize(cwd) + now: require("neoplen.path"):new(filepath):normalize(cwd) - read_file before: require("telescope.path").read_file(filepath) - now: require("plenary.path"):new(filepath):read() + now: require("neoplen.path"):new(filepath):read() - read_file_async before: require("telescope.path").read_file_async(filepath, callback) - now: require("plenary.path"):new(filepath):read(callback) + now: require("neoplen.path"):new(filepath):read(callback) *telescope.changelog-1406* diff --git a/lua/neoplen/async/async.lua b/lua/neoplen/async/async.lua new file mode 100644 index 0000000000..d850779f2f --- /dev/null +++ b/lua/neoplen/async/async.lua @@ -0,0 +1,135 @@ +local co = coroutine + +local traceback_error = function(s, level) + local traceback = debug.traceback() + traceback = traceback .. "\n" .. s + error(traceback, (level or 1) + 1) +end + +local function select_only(n) + return function(...) + local x = select(n, ...) + return x + end +end + +local first = select_only(1) +local second = select_only(2) +local third = select_only(3) + +local M = {} + +local function is_callable(fn) + return type(fn) == "function" or (type(fn) == "table" and type(getmetatable(fn)["__call"]) == "function") +end + +---because we can't store varargs +local function callback_or_next(step, thread, callback, ...) + local stat = first(...) + + if not stat then + error(string.format("The coroutine failed with this message: %s", second(...))) + end + + if co.status(thread) == "dead" then + if callback == nil then + return + end + callback(select(2, ...)) + else + local returned_function = second(...) + local nargs = third(...) + + assert(is_callable(returned_function), "type error :: expected func") + returned_function(require("neoplen.async.vararg").rotate(nargs, step, select(4, ...))) + end +end + +---Executes a future with a callback when it is done +---@param async_function Future: the future to execute +---@param callback function: the callback to call when done +local execute = function(async_function, callback, ...) + assert(is_callable(async_function), "type error :: expected func") + + local thread = co.create(async_function) + + local step + step = function(...) + callback_or_next(step, thread, callback, co.resume(thread, ...)) + end + + step(...) +end + +local add_leaf_function +do + ---A table to store all leaf async functions + _PlenaryLeafTable = setmetatable({}, { + __mode = "k", + }) + + add_leaf_function = function(async_func, argc) + assert(_PlenaryLeafTable[async_func] == nil, "Async function should not already be in the table") + _PlenaryLeafTable[async_func] = argc + end + + function M.is_leaf_function(async_func) + return _PlenaryLeafTable[async_func] ~= nil + end + + function M.get_leaf_function_argc(async_func) + return _PlenaryLeafTable[async_func] + end +end + +---Creates an async function with a callback style function. +---@param func function: A callback style function to be converted. The last argument must be the callback. +---@param argc number: The number of arguments of func. Must be included. +---@return function: Returns an async function +M.wrap = function(func, argc) + if not is_callable(func) then + traceback_error("type error :: expected func, got " .. type(func)) + end + + if type(argc) ~= "number" then + traceback_error("type error :: expected number, got " .. type(argc)) + end + + local function leaf(...) + local nargs = select("#", ...) + + if nargs == argc then + return func(...) + else + return co.yield(func, argc, ...) + end + end + + add_leaf_function(leaf, argc) + + return leaf +end + +---Use this to either run a future concurrently and then do something else +---or use it to run a future with a callback in a non async context +---@param async_function function +---@param callback function +M.run = function(async_function, callback) + if M.is_leaf_function(async_function) then + async_function(callback) + else + execute(async_function, callback) + end +end + +---Use this to create a function which executes in an async context but +---called from a non-async context. Inherently this cannot return anything +---since it is non-blocking +---@param func function +M.void = function(func) + return function(...) + execute(func, nil, ...) + end +end + +return M diff --git a/lua/neoplen/async/control.lua b/lua/neoplen/async/control.lua new file mode 100644 index 0000000000..5fb7eebfca --- /dev/null +++ b/lua/neoplen/async/control.lua @@ -0,0 +1,228 @@ +local a = require "neoplen.async.async" +local Dequeue = require "neoplen.async.dequeue" + +local M = {} + +local Condvar = {} +Condvar.__index = Condvar + +---@class Condvar +---@return Condvar +function Condvar.new() + return setmetatable({ handles = {} }, Condvar) +end + +---`blocks` the thread until a notification is received +Condvar.wait = a.wrap(function(self, callback) + -- not calling the callback will block the coroutine + table.insert(self.handles, callback) +end, 2) + +---notify everyone that is waiting on this Condvar +function Condvar:notify_all() + local len = #self.handles + for i, callback in ipairs(self.handles) do + if i > len then + -- this means that more handles were added while we were notifying + -- if we don't break we can get starvation notifying as soon as new handles are added + break + end + + callback() + end + + for _ = 1, len do + -- table.remove will ensure that indexes are correct and make "ipairs" safe, + -- which is not the case for "self.handles[i] = nil" + table.remove(self.handles) + end +end + +---notify randomly one person that is waiting on this Condvar +function Condvar:notify_one() + if #self.handles == 0 then + return + end + + local idx = math.random(#self.handles) + self.handles[idx]() + table.remove(self.handles, idx) +end + +M.Condvar = Condvar + +local Semaphore = {} +Semaphore.__index = Semaphore + +---@class Semaphore +---@param initial_permits number: the number of permits that it can give out +---@return Semaphore +function Semaphore.new(initial_permits) + vim.validate { + initial_permits = { + initial_permits, + function(n) + return n > 0 + end, + "number greater than 0", + }, + } + + return setmetatable({ permits = initial_permits, handles = {} }, Semaphore) +end + +---async function, blocks until a permit can be acquired +---example: +---local semaphore = Semaphore.new(1024) +---local permit = semaphore:acquire() +---permit:forget() +---when a permit can be acquired returns it +---call permit:forget() to forget the permit +Semaphore.acquire = a.wrap(function(self, callback) + if self.permits > 0 then + self.permits = self.permits - 1 + else + table.insert(self.handles, callback) + return + end + + local permit = {} + + permit.forget = function(self_permit) + self.permits = self.permits + 1 + + if self.permits > 0 and #self.handles > 0 then + self.permits = self.permits - 1 + table.remove(self.handles)(self_permit) + end + end + + callback(permit) +end, 2) + +M.Semaphore = Semaphore + +M.channel = {} + +---Creates a oneshot channel +---returns a sender and receiver function +---the sender is not async while the receiver is +---@return function, function +M.channel.oneshot = function() + local val = nil + local saved_callback = nil + local sent = false + local received = false + local is_single = false + + --- sender is not async + --- sends a value which can be nil + local sender = function(...) + assert(not sent, "Oneshot channel can only send once") + sent = true + + if saved_callback ~= nil then + saved_callback(...) + return + end + + -- optimise for when there is only one or zero argument, no need to pack + local nargs = select("#", ...) + if nargs == 1 or nargs == 0 then + val = ... + is_single = true + else + val = vim.F.pack_len(...) + end + end + + --- receiver is async + --- blocks until a value is received + local receiver = a.wrap(function(callback) + assert(not received, "Oneshot channel can only receive one value!") + + if sent then + received = true + if is_single then + return callback(val) + else + return callback(vim.F.unpack_len(val)) + end + else + saved_callback = callback + end + end, 1) + + return sender, receiver +end + +---A counter channel. +---Basically a channel that you want to use only to notify and not to send any actual values. +---@return function: sender +---@return function: receiver +M.channel.counter = function() + local counter = 0 + local condvar = Condvar.new() + + local Sender = {} + + function Sender:send() + counter = counter + 1 + condvar:notify_all() + end + + local Receiver = {} + + Receiver.recv = function() + if counter == 0 then + condvar:wait() + end + counter = counter - 1 + end + + Receiver.last = function() + if counter == 0 then + condvar:wait() + end + counter = 0 + end + + return Sender, Receiver +end + +---A multiple producer single consumer channel +---@return table +---@return table +M.channel.mpsc = function() + local deque = Dequeue.new() + local condvar = Condvar.new() + + local Sender = {} + + function Sender.send(...) + deque:pushleft { ... } + condvar:notify_all() + end + + local Receiver = {} + + Receiver.recv = function() + if deque:is_empty() then + condvar:wait() + end + return unpack(deque:popright()) + end + + Receiver.last = function() + if deque:is_empty() then + condvar:wait() + end + local val = deque:popleft() + deque:clear() + return unpack(val or {}) + end + + return Sender, Receiver +end + +return M diff --git a/lua/neoplen/async/dequeue.lua b/lua/neoplen/async/dequeue.lua new file mode 100644 index 0000000000..118df22b07 --- /dev/null +++ b/lua/neoplen/async/dequeue.lua @@ -0,0 +1,112 @@ +local Dequeue = {} +Dequeue.__index = Dequeue + +---@class Deque +---A double ended queue +--- +---@return Deque +function Dequeue.new() + -- the indexes are created with an offset so that the indices are consequtive + -- otherwise, when both pushleft and pushright are used, the indices will have a 1 length hole in the middle + return setmetatable({ first = 0, last = -1 }, Dequeue) +end + +---push to the left of the deque +---@param value any +function Dequeue:pushleft(value) + local first = self.first - 1 + self.first = first + self[first] = value +end + +---push to the right of the deque +---@param value any +function Dequeue:pushright(value) + local last = self.last + 1 + self.last = last + self[last] = value +end + +---pop from the left of the deque +---@return any +function Dequeue:popleft() + local first = self.first + if first > self.last then + return nil + end + local value = self[first] + self[first] = nil -- to allow garbage collection + self.first = first + 1 + return value +end + +---pops from the right of the deque +---@return any +function Dequeue:popright() + local last = self.last + if self.first > last then + return nil + end + local value = self[last] + self[last] = nil -- to allow garbage collection + self.last = last - 1 + return value +end + +---checks if the deque is empty +---@return boolean +function Dequeue:is_empty() + return self:len() == 0 +end + +---returns the number of elements of the deque +---@return number +function Dequeue:len() + return self.last - self.first + 1 +end + +---returns and iterator of the indices and values starting from the left +---@return function +function Dequeue:ipairs_left() + local i = self.first + + return function() + local res = self[i] + local idx = i + + if res then + i = i + 1 + + return idx, res + end + end +end + +---returns and iterator of the indices and values starting from the right +---@return function +function Dequeue:ipairs_right() + local i = self.last + + return function() + local res = self[i] + local idx = i + + if res then + i = i - 1 -- advance the iterator before we return + + return idx, res + end + end +end + +---removes all values from the deque +---@return nil +function Dequeue:clear() + for i, _ in self:ipairs_left() do + self[i] = nil + end + self.first = 0 + self.last = -1 +end + +return Dequeue diff --git a/lua/neoplen/async/init.lua b/lua/neoplen/async/init.lua new file mode 100644 index 0000000000..dbfb6e43a0 --- /dev/null +++ b/lua/neoplen/async/init.lua @@ -0,0 +1,25 @@ +---@brief [[ +--- NOTE: This API is still under construction. +--- It may change in the future :) +---@brief ]] + +local lookups = { + util = "neoplen.async.util", + control = "neoplen.async.control", +} + +local M = setmetatable(require "neoplen.async.async", { + __index = function(t, k) + local require_path = lookups[k] + if not require_path then + return + end + + local mod = require(require_path) + t[k] = mod + + return mod + end, +}) + +return M diff --git a/lua/neoplen/async/util.lua b/lua/neoplen/async/util.lua new file mode 100644 index 0000000000..9ba84e3b91 --- /dev/null +++ b/lua/neoplen/async/util.lua @@ -0,0 +1,143 @@ +local a = require "neoplen.async.async" +local control = require "neoplen.async.control" +local channel = control.channel + +local M = {} + +local defer_swapped = function(timeout, callback) + vim.defer_fn(callback, timeout) +end + +---Sleep for milliseconds +---@param ms number +M.sleep = a.wrap(defer_swapped, 2) + +---This will COMPLETELY block neovim +---please just use a.run unless you have a very special usecase +---for example, in plenary test_harness you must use this +---@param async_function Future +---@param timeout number: Stop blocking if the timeout was surpassed. Default 2000. +M.block_on = function(async_function, timeout) + async_function = M.protected(async_function) + + local stat + local ret = {} + + a.run(async_function, function(stat_, ...) + stat = stat_ + ret = { ... } + end) + + vim.wait(timeout or 2000, function() + return stat ~= nil + end, 20, false) + + if stat == false then + error(string.format("Blocking on future timed out or was interrupted.\n%s", unpack(ret))) + end + + return unpack(ret) +end + +---@see M.block_on +---@param async_function Future +---@param timeout number +M.will_block = function(async_function, timeout) + return function() + M.block_on(async_function, timeout) + end +end + +M.join = function(async_fns) + local len = #async_fns + local results = {} + if len == 0 then + return results + end + + local done = 0 + + local tx, rx = channel.oneshot() + + for i, async_fn in ipairs(async_fns) do + assert(type(async_fn) == "function", "type error :: future must be function") + + local cb = function(...) + results[i] = { ... } + done = done + 1 + if done == len then + tx() + end + end + + a.run(async_fn, cb) + end + + rx() + + return results +end + +---Returns a result from the future that finishes at the first +---@param async_functions table: The futures that you want to select +---@return ... +M.run_first = a.wrap(function(async_functions, step) + local ran = false + + for _, async_function in ipairs(async_functions) do + assert(type(async_function) == "function", "type error :: future must be function") + + local callback = function(...) + if not ran then + ran = true + step(...) + end + end + + async_function(callback) + end +end, 2) + +---Returns a result from the functions that finishes at the first +---@param funcs table: The async functions that you want to select +---@return ... +M.race = function(funcs) + local async_functions = vim.tbl_map(function(func) + return function(callback) + a.run(func, callback) + end + end, funcs) + return M.run_first(async_functions) +end + +M.run_all = function(async_fns, callback) + a.run(function() + M.join(async_fns) + end, callback) +end + +function M.apcall(async_fn, ...) + local nargs = a.get_leaf_function_argc(async_fn) + if nargs then + local tx, rx = channel.oneshot() + local stat, ret = pcall(async_fn, require("neoplen.async.vararg").rotate(nargs, tx, ...)) + if not stat then + return stat, ret + else + return stat, rx() + end + else + return pcall(async_fn, ...) + end +end + +function M.protected(async_fn) + return function() + return M.apcall(async_fn) + end +end + +---An async function that when called will yield to the neovim scheduler to be able to call the api. +M.scheduler = a.wrap(vim.schedule, 1) + +return M diff --git a/lua/neoplen/async/vararg.lua b/lua/neoplen/async/vararg.lua new file mode 100644 index 0000000000..6d34ebe46c --- /dev/null +++ b/lua/neoplen/async/vararg.lua @@ -0,0 +1,84 @@ +---@brief [[ +---Do not edit this file, it was generated! +---Provides a function to rotate a lua vararg +---@brief ]] + +local M = {} + +local rotate_lookup = {} + +rotate_lookup[1] = function(A0) + return A0 +end + +rotate_lookup[2] = function(A0, A1) + return A1, A0 +end + +rotate_lookup[3] = function(A0, A1, A2) + return A1, A2, A0 +end + +rotate_lookup[4] = function(A0, A1, A2, A3) + return A1, A2, A3, A0 +end + +rotate_lookup[5] = function(A0, A1, A2, A3, A4) + return A1, A2, A3, A4, A0 +end + +rotate_lookup[6] = function(A0, A1, A2, A3, A4, A5) + return A1, A2, A3, A4, A5, A0 +end + +rotate_lookup[7] = function(A0, A1, A2, A3, A4, A5, A6) + return A1, A2, A3, A4, A5, A6, A0 +end + +rotate_lookup[8] = function(A0, A1, A2, A3, A4, A5, A6, A7) + return A1, A2, A3, A4, A5, A6, A7, A0 +end + +rotate_lookup[9] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8) + return A1, A2, A3, A4, A5, A6, A7, A8, A0 +end + +rotate_lookup[10] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9) + return A1, A2, A3, A4, A5, A6, A7, A8, A9, A0 +end + +rotate_lookup[11] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10) + return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A0 +end + +rotate_lookup[12] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11) + return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A0 +end + +rotate_lookup[13] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12) + return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A0 +end + +rotate_lookup[14] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13) + return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A0 +end + +rotate_lookup[15] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14) + return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A0 +end + +local function rotate_n(first, ...) + local n = select("#", ...) + 1 + local args = vim.F.pack_len(...) + args[n] = first + return vim.F.unpack_len(args) +end + +M.rotate = function(nargs, ...) + if nargs == nil or nargs < 1 then + return + end + return (rotate_lookup[nargs] or rotate_n)(...) +end + +return M diff --git a/lua/neoplen/border.lua b/lua/neoplen/border.lua new file mode 100644 index 0000000000..3d887a768c --- /dev/null +++ b/lua/neoplen/border.lua @@ -0,0 +1,302 @@ +local api = vim.api +local strings = require "neoplen.strings" + +local Border = {} + +Border.__index = Border + +Border._default_thickness = { + top = 1, + right = 1, + bot = 1, + left = 1, +} + +local calc_left_start = function(title_pos, title_len, total_width) + if string.find(title_pos, "W") then + return 0 + elseif string.find(title_pos, "E") then + return total_width - title_len + else + return math.floor((total_width - title_len) / 2) + end +end + +local create_horizontal_line = function(title, pos, width, left_char, mid_char, right_char) + local title_len + if title == "" then + title_len = 0 + else + local len = strings.strdisplaywidth(title) + local max_title_width = width - 2 + if len > max_title_width then + title = strings.truncate(title, max_title_width) + len = strings.strdisplaywidth(title) + end + title = string.format(" %s ", title) + title_len = len + 2 + end + + local left_start = calc_left_start(pos, title_len, width) + + local horizontal_line = string.format( + "%s%s%s%s%s", + left_char, + string.rep(mid_char, left_start), + title, + string.rep(mid_char, width - title_len - left_start), + right_char + ) + local ranges = {} + if title_len ~= 0 then + -- Need to calculate again due to multi-byte characters + local r_start = string.len(left_char) + math.max(left_start, 0) * string.len(mid_char) + ranges = { { r_start, r_start + string.len(title) } } + end + return horizontal_line, ranges +end + +function Border._create_lines(content_win_id, content_win_options, border_win_options) + local content_pos = api.nvim_win_get_position(content_win_id) + local content_height = api.nvim_win_get_height(content_win_id) + local content_width = api.nvim_win_get_width(content_win_id) + + -- TODO: Handle border width, which I haven't right here. + local thickness = border_win_options.border_thickness + + local top_enabled = thickness.top == 1 + local right_enabled = thickness.right == 1 and content_pos[2] + content_width < vim.o.columns + local bot_enabled = thickness.bot == 1 + local left_enabled = thickness.left == 1 and content_pos[2] > 0 + + border_win_options.border_thickness.left = left_enabled and 1 or 0 + border_win_options.border_thickness.right = right_enabled and 1 or 0 + + local border_lines = {} + local ranges = {} + + -- border_win_options.title should have be a list with entries of the + -- form: { pos = foo, text = bar }. + -- pos can take values in { "NW", "N", "NE", "SW", "S", "SE" } + local titles = type(border_win_options.title) == "string" and { { pos = "N", text = border_win_options.title } } + or border_win_options.title + or {} + + local topline = nil + local topleft = (left_enabled and border_win_options.topleft) or "" + local topright = (right_enabled and border_win_options.topright) or "" + -- Only calculate the topline if there is space above the first content row (relative to the editor) + if content_pos[1] > 0 then + for _, title in ipairs(titles) do + if string.find(title.pos, "N") then + local top_ranges + topline, top_ranges = create_horizontal_line( + title.text, + title.pos, + content_win_options.width, + topleft, + border_win_options.top or "", + topright + ) + for _, r in pairs(top_ranges) do + table.insert(ranges, { 0, r[1], r[2] }) + end + break + end + end + if topline == nil then + if top_enabled then + topline = topleft .. string.rep(border_win_options.top, content_win_options.width) .. topright + end + end + else + border_win_options.border_thickness.top = 0 + end + + if topline then + table.insert(border_lines, topline) + end + + local middle_line = string.format( + "%s%s%s", + (left_enabled and border_win_options.left) or "", + string.rep(" ", content_win_options.width), + (right_enabled and border_win_options.right) or "" + ) + + for _ = 1, content_win_options.height do + table.insert(border_lines, middle_line) + end + + local botline = nil + local botleft = (left_enabled and border_win_options.botleft) or "" + local botright = (right_enabled and border_win_options.botright) or "" + if content_pos[1] + content_height < vim.o.lines then + for _, title in ipairs(titles) do + if string.find(title.pos, "S") then + local bot_ranges + botline, bot_ranges = create_horizontal_line( + title.text, + title.pos, + content_win_options.width, + botleft, + border_win_options.bot or "", + botright + ) + for _, r in pairs(bot_ranges) do + table.insert(ranges, { content_win_options.height + thickness.top, r[1], r[2] }) + end + break + end + end + if botline == nil then + if bot_enabled then + botline = botleft .. string.rep(border_win_options.bot, content_win_options.width) .. botright + end + end + else + border_win_options.border_thickness.bot = 0 + end + + if botline then + table.insert(border_lines, botline) + end + + return border_lines, ranges +end + +local set_title_highlights = function(bufnr, ranges, hl) + -- Check if both `hl` and `ranges` are provided, and `ranges` is not the empty table. + if hl and ranges and next(ranges) then + local ns = api.nvim_create_namespace "neoplen_border" + for _, r in pairs(ranges) do + vim.hl.range(bufnr, ns, hl, { r[1], r[2] }, { r[1], r[3] }) + end + end +end + +function Border:change_title(new_title, pos) + if self._border_win_options.title == new_title then + return + end + + pos = pos + or (self._border_win_options.title and self._border_win_options.title[1] and self._border_win_options.title[1].pos) + if pos == nil then + self._border_win_options.title = new_title + else + self._border_win_options.title = { { text = new_title, pos = pos } } + end + + self.contents, self.title_ranges = + Border._create_lines(self.content_win_id, self.content_win_options, self._border_win_options) + api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents) + + set_title_highlights(self.bufnr, self.title_ranges, self._border_win_options.titlehighlight) +end + +-- Updates characters for border lines, and returns nvim_win_config +-- (generally used in conjunction with `move` or `new`) +function Border:__align_calc_config(content_win_options, border_win_options) + border_win_options = vim.tbl_deep_extend("keep", border_win_options, { + border_thickness = Border._default_thickness, + + -- Border options, could be passed as a list? + topleft = "╔", + topright = "╗", + top = "═", + left = "║", + right = "║", + botleft = "╚", + botright = "╝", + bot = "═", + }) + + -- Ensure the relevant contents and border win_options are set + self._border_win_options = border_win_options + self.content_win_options = content_win_options + -- Update border characters and title_ranges + self.contents, self.title_ranges = Border._create_lines(self.content_win_id, content_win_options, border_win_options) + + api.nvim_set_option_value("modifiable", true, { buf = self.bufnr }) + api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents) + + local thickness = border_win_options.border_thickness + local nvim_win_config = { + anchor = content_win_options.anchor, + relative = content_win_options.relative, + style = "minimal", + row = content_win_options.row - thickness.top, + col = content_win_options.col - thickness.left, + width = content_win_options.width + thickness.left + thickness.right, + height = content_win_options.height + thickness.top + thickness.bot, + zindex = content_win_options.zindex or 50, + noautocmd = content_win_options.noautocmd, + focusable = vim.F.if_nil(border_win_options.focusable, false), + border = "none", + } + + return nvim_win_config +end + +-- Sets the size and position of the given Border. +-- Can be used to create a new window (with `create_window = true`) +-- or change an existing one +function Border:move(content_win_options, border_win_options) + -- Update lines in border buffer, and get config for border window + local nvim_win_config = self:__align_calc_config(content_win_options, border_win_options) + + -- Set config for border window + api.nvim_win_set_config(self.win_id, nvim_win_config) + + set_title_highlights(self.bufnr, self.title_ranges, self._border_win_options.titlehighlight) +end + +function Border:new(content_bufnr, content_win_id, content_win_options, border_win_options) + assert(type(content_win_id) == "number", "Must supply a valid win_id. It's possible you forgot to call with ':'") + + local obj = {} + + obj.content_win_id = content_win_id + + obj.bufnr = api.nvim_create_buf(false, true) + assert(obj.bufnr, "Failed to create border buffer") + api.nvim_set_option_value("bufhidden", "wipe", { buf = obj.bufnr }) + + -- Create a border window and buffer, with border characters around the edge + local nvim_win_config = Border.__align_calc_config(obj, content_win_options, border_win_options) + obj.win_id = api.nvim_open_win(obj.bufnr, false, nvim_win_config) + + if border_win_options.highlight then + api.nvim_set_option_value("winhl", border_win_options.highlight, { win = obj.win_id }) + end + + set_title_highlights(obj.bufnr, obj.title_ranges, obj._border_win_options.titlehighlight) + + local augroup = api.nvim_create_augroup("neoplen.border", {}) + api.nvim_create_autocmd("BufDelete", { + group = augroup, + buffer = content_bufnr, + once = true, + nested = true, + callback = function() + pcall(api.nvim_win_close, content_win_id, true) + pcall(api.nvim_win_close, obj.win_id, true) + end, + }) + api.nvim_create_autocmd("WinClosed", { + group = augroup, + buffer = content_bufnr, + once = true, + nested = true, + callback = function() + pcall(api.nvim_win_close, obj.win_id, true) + end, + }) + + setmetatable(obj, Border) + + return obj +end + +return Border diff --git a/lua/neoplen/class.lua b/lua/neoplen/class.lua new file mode 100644 index 0000000000..2aaa1d8281 --- /dev/null +++ b/lua/neoplen/class.lua @@ -0,0 +1,51 @@ +---@brief [[ +---classic +--- +---Copyright (c) 2014, rxi +---@brief ]] + +---@class Object +local Object = {} +Object.__index = Object + +---Does nothing. +---You have to implement this yourself for extra functionality when initializing +---@param self Object +function Object:new() end + +---Create a new class/object by extending the base Object class. +---The extended object will have a field called `super` that will access the super class. +---@param self Object +---@return Object +function Object:extend() + local cls = {} + for k, v in pairs(self) do + if k:find "__" == 1 then + cls[k] = v + end + end + cls.__index = cls + cls.super = self + setmetatable(cls, self) + return cls +end + +---The default tostring implementation for an object. +---You can override this to provide a different tostring. +---@param self Object +---@return string +function Object:__tostring() + return "Object" +end + +---You can call the class the initialize it without using `Object:new`. +---@param self Object +---@param nil ... +---@return Object +function Object:__call(...) + local obj = setmetatable({}, self) + obj:new(...) + return obj +end + +return Object diff --git a/lua/neoplen/job.lua b/lua/neoplen/job.lua new file mode 100644 index 0000000000..f4e9caf12e --- /dev/null +++ b/lua/neoplen/job.lua @@ -0,0 +1,678 @@ +local api = vim.api +local uv = vim.uv +local F = vim.F + +---@class Job +---@field command string Command to run +---@field args? string[] List of arguments to pass +---@field cwd? string Working directory for job +---@field env? table|string[] Environment looking like: { ['VAR'] = 'VALUE' } or { 'VAR=VALUE' } +---@field interactive? boolean +---@field detached? boolean Spawn the child in a detached state making it a process group leader +---@field skip_validation? boolean Skip validating the arguments +---@field enable_handlers? boolean If set to false, disables all callbacks associated with output (default: true) +---@field enable_recording? boolean +---@field on_start? fun() +---@field on_stdout? fun(error: string, data: string, self?: Job) +---@field on_stderr? fun(error: string, data: string, self?: Job) +---@field on_exit? fun(self: Job, code: number, signal: number) +---@field maximum_results? number Stop processing results after this number +---@field writer? Job|table|string Job that writes to stdin of this job. +local Job = {} +Job.__index = Job + +local function close_safely(j, key) + local handle = j[key] + + if not handle then + return + end + + if not handle:is_closing() then + handle:close() + end +end + +local start_shutdown_check = function(child, options, code, signal) + uv.check_start(child._shutdown_check, function() + if not child:_pipes_are_closed(options) then + return + end + + -- Wait until all the pipes are closing. + uv.check_stop(child._shutdown_check) + child._shutdown_check = nil + + child:_shutdown(code, signal) + + -- Remove left over references + child = nil + end) +end + +local shutdown_factory = function(child, options) + return function(code, signal) + if uv.is_closing(child._shutdown_check) then + return child:shutdown(code, signal) + else + start_shutdown_check(child, options, code, signal) + end + end +end + +local function expand(path) + if vim.in_fast_event() then + return assert(uv.fs_realpath(path), string.format("Path must be valid: %s", path)) + else + -- TODO: Probably want to check that this is valid here... otherwise that's weird. + return vim.fn.expand(vim.fn.escape(path, "[]$"), true) + end +end + +---@class Array +--- Numeric table + +---@class Map +--- Map-like table + +---Create a new job +---@param o Job +---@return Job +function Job:new(o) + if not o then + error(debug.traceback "Options are required for Job:new") + end + + local command = o.command + if not command then + if o[1] then + command = o[1] + else + error(debug.traceback "'command' is required for Job:new") + end + elseif o[1] then + error(debug.traceback "Cannot pass both 'command' and array args") + end + + local args = o.args + if not args then + if #o > 1 then + args = { select(2, unpack(o)) } + end + end + + local ok, is_exe = pcall(vim.fn.executable, command) + if not o.skip_validation and ok and 1 ~= is_exe then + error(debug.traceback(command .. ": Executable not found")) + end + + local obj = {} + + obj.command = command + obj.args = args + obj._raw_cwd = o.cwd + if o.env then + if type(o.env) ~= "table" then + error "[plenary.job] env has to be a table" + end + + local transform = {} + for k, v in pairs(o.env) do + if type(k) == "number" then + table.insert(transform, v) + elseif type(k) == "string" then + table.insert(transform, k .. "=" .. tostring(v)) + end + end + obj.env = transform + end + if o.interactive == nil then + obj.interactive = true + else + obj.interactive = o.interactive + end + + if o.detached then + obj.detached = true + end + + -- enable_handlers: Do you want to do ANYTHING with the stdout/stderr of the proc + obj.enable_handlers = F.if_nil(o.enable_handlers, true, o.enable_handlers) + + -- enable_recording: Do you want to record stdout/stderr into a table. + -- Since it cannot be enabled when enable_handlers is false, + -- we try and make sure they are associated correctly. + obj.enable_recording = + F.if_nil(F.if_nil(o.enable_recording, o.enable_handlers, o.enable_recording), true, o.enable_recording) + + if not obj.enable_handlers and obj.enable_recording then + error "[plenary.job] Cannot record items but disable handlers" + end + + obj._user_on_start = o.on_start + obj._user_on_stdout = o.on_stdout + obj._user_on_stderr = o.on_stderr + obj._user_on_exit = o.on_exit + + obj._additional_on_exit_callbacks = {} + + obj._maximum_results = o.maximum_results + + obj.user_data = {} + + obj.writer = o.writer + + self._reset(obj) + + return setmetatable(obj, self) +end + +function Job:_reset() + self.is_shutdown = nil + + if self._shutdown_check and uv.is_active(self._shutdown_check) and not uv.is_closing(self._shutdown_check) then + api.nvim_echo({ { debug.traceback "We may be memory leaking here. Please report to TJ." } }, true, { err = true }) + end + self._shutdown_check = uv.new_check() + + self.stdin = nil + self.stdout = nil + self.stderr = nil + + self._stdout_reader = nil + self._stderr_reader = nil + + if self.enable_recording then + self._stdout_results = {} + self._stderr_results = {} + else + self._stdout_results = nil + self._stderr_results = nil + end +end + +--- Stop a job and close all handles +function Job:_stop() + close_safely(self, "stdin") + close_safely(self, "stderr") + close_safely(self, "stdout") + close_safely(self, "handle") +end + +function Job:_pipes_are_closed(options) + for _, pipe in ipairs { options.stdin, options.stdout, options.stderr } do + if pipe and not uv.is_closing(pipe) then + return false + end + end + + return true +end + +--- Shutdown a job. +function Job:shutdown(code, signal) + if self._shutdown_check and uv.is_active(self._shutdown_check) then + -- shutdown has already started + return + end + + self:_shutdown(code, signal) +end + +function Job:_shutdown(code, signal) + if self.is_shutdown then + return + end + + self.code = code + self.signal = signal + + if self._stdout_reader then + pcall(self._stdout_reader, nil, nil, true) + end + + if self._stderr_reader then + pcall(self._stderr_reader, nil, nil, true) + end + + if self._user_on_exit then + self:_user_on_exit(code, signal) + end + + for _, v in ipairs(self._additional_on_exit_callbacks) do + v(self, code, signal) + end + + if self.stdout then + self.stdout:read_stop() + end + + if self.stderr then + self.stderr:read_stop() + end + + self:_stop() + + self.is_shutdown = true + + self._stdout_reader = nil + self._stderr_reader = nil +end + +function Job:_create_uv_options() + local options = {} + + options.command = self.command + options.args = self.args + options.stdio = { self.stdin, self.stdout, self.stderr } + + if self._raw_cwd then + options.cwd = expand(self._raw_cwd) + end + if self.env then + options.env = self.env + end + + if self.detached then + options.detached = true + end + + return options +end + +local on_output = function(self, result_key, cb) + return coroutine.wrap(function(err, data, is_complete) + local result_index = 1 + + local line, start, result_line, found_newline + + -- We repeat forever as a coroutine so that we can keep calling this. + while true do + if data then + data = data:gsub("\r", "") + + local processed_index = 1 + local data_length = #data + 1 + + repeat + start = string.find(data, "\n", processed_index, true) or data_length + line = string.sub(data, processed_index, start - 1) + found_newline = start ~= data_length + + -- Concat to last line if there was something there already. + -- This happens when "data" is broken into chunks and sometimes + -- the content is sent without any newlines. + if result_line then + -- results[result_index] = results[result_index] .. line + result_line = result_line .. line + + -- Only put in a new line when we actually have new data to split. + -- This is generally only false when we do end with a new line. + -- It prevents putting in a "" to the end of the results. + elseif start ~= processed_index or found_newline then + -- results[result_index] = line + result_line = line + + -- Otherwise, we don't need to do anything. + end + + if found_newline then + if not result_line then + return api.nvim_echo( + { { "Broken data thing due to: " .. tostring(result_line) .. " " .. tostring(data) } }, + true, + { err = true } + ) + end + + if self.enable_recording then + self[result_key][result_index] = result_line + end + + if cb then + cb(err, result_line, self) + end + + -- Stop processing if we've surpassed the maximum. + if self._maximum_results and result_index > self._maximum_results then + -- Shutdown once we get the chance. + -- Can't call it here, because we'll just keep calling ourselves. + vim.schedule(function() + self:shutdown() + end) + + return + end + + result_index = result_index + 1 + result_line = nil + end + + processed_index = start + 1 + until not found_newline + end + + if self.enable_recording then + self[result_key][result_index] = result_line + end + + -- If we didn't get a newline on the last execute, send the final results. + if cb and is_complete and not found_newline then + cb(err, result_line, self) + end + + if is_complete then + return + end + + err, data, is_complete = coroutine.yield() + end + end) +end + +--- Stop previous execution and add new pipes. +--- Also regenerates pipes of writer. +function Job:_prepare_pipes() + self:_stop() + + if self.writer then + if Job.is_job(self.writer) then + self.writer:_prepare_pipes() + self.stdin = self.writer.stdout + elseif self.writer.write then + self.stdin = self.writer + end + end + + if not self.stdin then + self.stdin = self.interactive and uv.new_pipe(false) or nil + end + + self.stdout = uv.new_pipe(false) + self.stderr = uv.new_pipe(false) +end + +--- Execute job. Should be called only after preprocessing is done. +function Job:_execute() + local options = self:_create_uv_options() + + if self._user_on_start then + self:_user_on_start() + end + + self.handle, self.pid = uv.spawn(options.command, options, shutdown_factory(self, options)) + + if not self.handle then + error(debug.traceback("Failed to spawn process: " .. vim.inspect(self))) + end + + if self.enable_handlers then + self._stdout_reader = on_output(self, "_stdout_results", self._user_on_stdout) + self.stdout:read_start(self._stdout_reader) + + self._stderr_reader = on_output(self, "_stderr_results", self._user_on_stderr) + self.stderr:read_start(self._stderr_reader) + end + + if self.writer then + if Job.is_job(self.writer) then + self.writer:_execute() + elseif type(self.writer) == "table" and vim.islist(self.writer) then + local writer_len = #self.writer + for i, v in ipairs(self.writer) do + self.stdin:write(v) + if i ~= writer_len then + self.stdin:write "\n" + else + self.stdin:write("\n", function() + pcall(self.stdin.close, self.stdin) + end) + end + end + elseif type(self.writer) == "string" then + self.stdin:write(self.writer, function() + self.stdin:close() + end) + elseif self.writer.write then + self.stdin = self.writer + else + error("Unknown self.writer: " .. vim.inspect(self.writer)) + end + end + + return self +end + +function Job:start() + self:_reset() + self:_prepare_pipes() + self:_execute() +end + +function Job:sync(timeout, wait_interval) + self:start() + self:wait(timeout, wait_interval) + + return self.enable_recording and self:result() or nil, self.code +end + +function Job:result() + assert(self.enable_recording, "'enable_recording' is not enabled for this job.") + return self._stdout_results +end + +function Job:stderr_result() + assert(self.enable_recording, "'enable_recording' is not enabled for this job.") + return self._stderr_results +end + +function Job:pid() + return self.pid +end + +function Job:wait(timeout, wait_interval, should_redraw) + timeout = timeout or 5000 + wait_interval = wait_interval or 10 + + if self.handle == nil then + local msg = vim.inspect(self) + vim.schedule(function() + api.nvim_echo({ { msg } }, true, { err = true }) + end) + + return + end + + -- Wait five seconds, or until timeout. + local wait_result = vim.wait(timeout, function() + if should_redraw then + vim.cmd [[redraw!]] + end + + if self.is_shutdown then + assert(not self.handle or self.handle:is_closing(), "Job must be shutdown if it's closing") + end + + return self.is_shutdown + end, wait_interval, not should_redraw) + + if not wait_result then + error( + string.format( + "'%s %s' was unable to complete in %s ms", + self.command, + table.concat(self.args or {}, " "), + timeout + ) + ) + end + + return self +end + +function Job:co_wait(wait_time) + wait_time = wait_time or 5 + + if self.handle == nil then + api.nvim_echo({ { vim.inspect(self) } }, true, { err = true }) + return + end + + while not vim.wait(wait_time, function() + return self.is_shutdown + end) do + coroutine.yield() + end + + return self +end + +--- Wait for all jobs to complete +function Job.join(...) + local jobs_to_wait = { ... } + local num_jobs = #jobs_to_wait + + -- last entry can be timeout + local timeout + if type(jobs_to_wait[num_jobs]) == "number" then + timeout = table.remove(jobs_to_wait, num_jobs) + num_jobs = num_jobs - 1 + end + + local completed = 0 + + return vim.wait(timeout or 10000, function() + for index, current_job in pairs(jobs_to_wait) do + if current_job.is_shutdown then + jobs_to_wait[index] = nil + completed = completed + 1 + end + end + + return num_jobs == completed + end) +end + +local _request_id = 0 +local _request_status = {} + +function Job:and_then(next_job) + self:add_on_exit_callback(function() + next_job:start() + end) +end + +function Job:and_then_wrap(next_job) + self:add_on_exit_callback(vim.schedule_wrap(function() + next_job:start() + end)) +end + +function Job:after(fn) + self:add_on_exit_callback(fn) + return self +end + +function Job:and_then_on_success(next_job) + self:add_on_exit_callback(function(_, code) + if code == 0 then + next_job:start() + end + end) +end + +function Job:and_then_on_success_wrap(next_job) + self:add_on_exit_callback(vim.schedule_wrap(function(_, code) + if code == 0 then + next_job:start() + end + end)) +end + +function Job:after_success(fn) + self:add_on_exit_callback(function(j, code, signal) + if code == 0 then + fn(j, code, signal) + end + end) +end + +function Job:and_then_on_failure(next_job) + self:add_on_exit_callback(function(_, code) + if code ~= 0 then + next_job:start() + end + end) +end + +function Job:and_then_on_failure_wrap(next_job) + self:add_on_exit_callback(vim.schedule_wrap(function(_, code) + if code ~= 0 then + next_job:start() + end + end)) +end + +function Job:after_failure(fn) + self:add_on_exit_callback(function(j, code, signal) + if code ~= 0 then + fn(j, code, signal) + end + end) +end + +function Job.chain(...) + _request_id = _request_id + 1 + _request_status[_request_id] = false + + local jobs = { ... } + + for index = 2, #jobs do + local prev_job = jobs[index - 1] + local job = jobs[index] + + prev_job:add_on_exit_callback(vim.schedule_wrap(function() + job:start() + end)) + end + + local last_on_exit = jobs[#jobs]._user_on_exit + jobs[#jobs]._user_on_exit = function(self, err, data) + if last_on_exit then + last_on_exit(self, err, data) + end + + _request_status[_request_id] = true + end + + jobs[1]:start() + + return _request_id +end + +function Job.chain_status(id) + return _request_status[id] +end + +function Job.is_job(item) + if type(item) ~= "table" then + return false + end + + return getmetatable(item) == Job +end + +function Job:add_on_exit_callback(cb) + table.insert(self._additional_on_exit_callbacks, cb) +end + +--- Send data to a job. +function Job:send(data) + if not self.stdin then + error "job has no 'stdin'. Have you run `job:start()` yet?" + end + + self.stdin:write(data) +end + +return Job diff --git a/lua/neoplen/log.lua b/lua/neoplen/log.lua new file mode 100644 index 0000000000..7f5c0eb93e --- /dev/null +++ b/lua/neoplen/log.lua @@ -0,0 +1,217 @@ +-- log.lua +-- Does only support logging source files. +-- +-- Inspired by rxi/log.lua +-- Modified by tjdevries and can be found at github.com/tjdevries/vlog.nvim +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. + +-- User configuration section +local default_config = { + -- Name of the plugin. Prepended to log messages. + plugin = "neoplen", + + -- Should print the output to neovim while running. + -- values: 'sync','async',false + use_console = "async", + + -- Should highlighting be used in console (using echohl). + highlights = true, + + -- Should write to a file. + -- Default output for logging file is `stdpath("log")/plugin.log`. + use_file = true, + + -- Should write to the quickfix list. + use_quickfix = false, + + -- Any messages above this level will be logged. + level = "info", + + -- Level configuration. + modes = { + { name = "trace", hl = "Comment" }, + { name = "debug", hl = "Comment" }, + { name = "info", hl = "None" }, + { name = "warn", hl = "WarningMsg" }, + { name = "error", hl = "ErrorMsg" }, + { name = "fatal", hl = "ErrorMsg" }, + }, + + -- Can limit the number of decimals displayed for floats. + float_precision = 0.01, + + -- Adjust content as needed, but must keep function parameters to be filled + -- by library code. + ---@param is_console boolean If output is for console or log file. + ---@param mode_name string Level configuration 'modes' field 'name' + ---@param src_path string Path to source file given by debug.info.source + ---@param src_line integer Line into source file given by debug.info.currentline + ---@param msg string Message, which is later on escaped, if needed. + fmt_msg = function(is_console, mode_name, src_path, src_line, msg) + local nameupper = mode_name:upper() + local lineinfo = src_path .. ":" .. src_line + if is_console then + return string.format("[%-6s%s] %s: %s", nameupper, os.date "%H:%M:%S", lineinfo, msg) + else + return string.format("[%-6s%s] %s: %s\n", nameupper, os.date(), lineinfo, msg) + end + end, +} + +-- {{{ NO NEED TO CHANGE +local log = {} + +local unpack = unpack or table.unpack + +log.new = function(config, standalone) + config = vim.tbl_deep_extend("force", default_config, config) + + local outfile = vim.F.if_nil(config.outfile, vim.fs.joinpath(vim.fn.stdpath "log", config.plugin .. ".log")) + + local obj + if standalone then + obj = log + else + obj = config + end + + local levels = {} + for i, v in ipairs(config.modes) do + levels[v.name] = i + end + + local round = function(x, increment) + if x == 0 then + return x + end + increment = increment or 1 + x = x / increment + return (x > 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)) * increment + end + + local make_string = function(...) + local t = {} + for i = 1, select("#", ...) do + local x = select(i, ...) + + if type(x) == "number" and config.float_precision then + x = tostring(round(x, config.float_precision)) + elseif type(x) == "table" then + x = vim.inspect(x) + else + x = tostring(x) + end + + t[#t + 1] = x + end + return table.concat(t, " ") + end + + local log_at_level = function(level, level_config, message_maker, ...) + -- Return early if we're below the config.level + if level < levels[config.level] then + return + end + local msg = message_maker(...) + local info = debug.getinfo(config.info_level or 2, "Sl") + local src_path = info.source:sub(2) + local src_line = info.currentline + -- Output to console + if config.use_console then + local log_to_console = function() + local console_string = config.fmt_msg(true, level_config.name, src_path, src_line, msg) + + if config.highlights and level_config.hl then + vim.cmd(string.format("echohl %s", level_config.hl)) + end + + local split_console = vim.split(console_string, "\n") + for _, v in ipairs(split_console) do + local formatted_msg = string.format("[%s] %s", config.plugin, vim.fn.escape(v, [["\]])) + + local ok = pcall(vim.cmd, string.format([[echom "%s"]], formatted_msg)) + if not ok then + vim.api.nvim_echo({ { msg } }, true, {}) + end + end + + if config.highlights and level_config.hl then + vim.cmd "echohl NONE" + end + end + if config.use_console == "sync" and not vim.in_fast_event() then + log_to_console() + else + vim.schedule(log_to_console) + end + end + + -- Output to log file + if config.use_file then + local fp = assert(io.open(outfile, "a")) + local str = config.fmt_msg(false, level_config.name, src_path, src_line, msg) + fp:write(str) + fp:close() + end + + -- Output to quickfix + if config.use_quickfix then + local nameupper = level_config.name:upper() + local formatted_msg = string.format("[%s] %s", nameupper, msg) + local qf_entry = { + -- remove the @ getinfo adds to the file path + filename = info.source:sub(2), + lnum = info.currentline, + col = 1, + text = formatted_msg, + } + vim.fn.setqflist({ qf_entry }, "a") + end + end + + for i, x in ipairs(config.modes) do + -- log.info("these", "are", "separated") + obj[x.name] = function(...) + return log_at_level(i, x, make_string, ...) + end + + -- log.fmt_info("These are %s strings", "formatted") + obj[("fmt_%s"):format(x.name)] = function(...) + return log_at_level(i, x, function(...) + local passed = { ... } + local fmt = table.remove(passed, 1) + local inspected = {} + for _, v in ipairs(passed) do + table.insert(inspected, vim.inspect(v)) + end + return string.format(fmt, unpack(inspected)) + end, ...) + end + + -- log.lazy_info(expensive_to_calculate) + obj[("lazy_%s"):format(x.name)] = function() + return log_at_level(i, x, function(f) + return f() + end) + end + + -- log.file_info("do not print") + obj[("file_%s"):format(x.name)] = function(vals, override) + local original_console = config.use_console + config.use_console = false + config.info_level = override.info_level + log_at_level(i, x, make_string, unpack(vals)) + config.use_console = original_console + config.info_level = nil + end + end + + return obj +end + +log.new(default_config, true) +-- }}} + +return log diff --git a/lua/neoplen/path.lua b/lua/neoplen/path.lua new file mode 100644 index 0000000000..3dc54eef6a --- /dev/null +++ b/lua/neoplen/path.lua @@ -0,0 +1,196 @@ +--- Path.lua +--- +--- Goal: Create objects that are extremely similar to Python's `Path` Objects. +--- Reference: https://docs.python.org/3/library/pathlib.html + +local uv = vim.uv +local fs = vim.fs + +---@class Path +---@field filename string +local Path = {} + +local check_self = function(self) + if type(self) == "string" then + return Path:new(self) + end + + return self +end + +Path.__index = function(t, k) + local raw = rawget(Path, k) + if raw then + return raw + end + + if k == "_cwd" then + local cwd = uv.fs_realpath "." + t._cwd = cwd + return cwd + end + + if k == "_absolute" then + local absolute = uv.fs_realpath(t.filename) + t._absolute = absolute + return absolute + end +end + +Path.__tostring = function(self) + return fs.normalize(self.filename) +end + +Path.is_path = function(a) + return getmetatable(a) == Path +end + +function Path:new(...) + local args = { ... } + + if type(self) == "string" then + table.insert(args, 1, self) + self = Path -- luacheck: ignore + end + + local path_input + if #args == 1 then + path_input = args[1] + else + path_input = args + end + + -- If we already have a Path, it's fine. + -- Just return it + if Path.is_path(path_input) then + return path_input + end + + local path_string + if type(path_input) == "table" then + -- TODO: It's possible this could be done more elegantly with __concat + -- But I'm unsure of what we'd do to make that happen + local path_objs = {} + for _, v in ipairs(path_input) do + if Path.is_path(v) then + table.insert(path_objs, v.filename) + else + assert(type(v) == "string") + table.insert(path_objs, v) + end + end + + local pathsep = vim.fn.has "win32" == 1 and "\\" or "/" + path_string = table.concat(path_objs, pathsep) + else + assert(type(path_input) == "string", vim.inspect(path_input)) + path_string = path_input + end + + local obj = { + filename = path_string, + } + + setmetatable(obj, Path) + + return obj +end + +function Path:_fs_filename() + return self:absolute() or self.filename +end + +function Path:_stat() + return uv.fs_stat(self:_fs_filename()) or {} +end + +function Path:absolute() + return fs.abspath(self.filename) +end + +function Path:exists() + return not vim.tbl_isempty(self:_stat()) +end + +function Path:make_relative(cwd) + return fs.relpath(cwd, self.filename) or self.filename +end + +function Path:normalize(_) + return fs.normalize(self.filename) +end + +function Path:is_file() + return self:_stat().type == "file" and true or nil +end + +-- TODO: Asyncify this and use vim.wait in the meantime. +-- This will allow other events to happen while we're waiting! +function Path:read() + self = check_self(self) + + local fd = assert(uv.fs_open(self:_fs_filename(), "r", 438)) -- for some reason test won't pass with absolute + local stat = assert(uv.fs_fstat(fd)) + local data = assert(uv.fs_read(fd, stat.size, 0)) + assert(uv.fs_close(fd)) + + return data +end + +function Path:touch(opts) + opts = opts or {} + + local mode = opts.mode or 420 + local parents = vim.F.if_nil(opts.parents, false, opts.parents) + + if self:exists() then + local new_time = os.time() + uv.fs_utime(self:_fs_filename(), new_time, new_time) + return + end + + if parents then + vim.fn.mkdir(fs.dirname(self.filename), "p") + end + + local fd = uv.fs_open(self:_fs_filename(), "w", mode) + if not fd then + error("Could not create file: " .. self:_fs_filename()) + end + uv.fs_close(fd) + + return true +end + +function Path:_read_async(callback) + uv.fs_open(self.filename, "r", 438, function(err_open, fd) + if err_open then + print("We tried to open this file but couldn't. We failed with following error message: " .. err_open) + return + end + uv.fs_fstat(fd, function(err_fstat, stat) + assert(not err_fstat, err_fstat) + if stat.type ~= "file" then + return callback "" + end + uv.fs_read(fd, stat.size, 0, function(err_read, data) + assert(not err_read, err_read) + uv.fs_close(fd, function(err_close) + assert(not err_close, err_close) + return callback(data) + end) + end) + end) + end) +end + +function Path:readlines() + self = check_self(self) + + local data = self:read() + + data = data:gsub("\r", "") + return vim.split(data, "\n") +end + +return Path diff --git a/lua/neoplen/popup.lua b/lua/neoplen/popup.lua new file mode 100644 index 0000000000..0eb789cfcf --- /dev/null +++ b/lua/neoplen/popup.lua @@ -0,0 +1,418 @@ +--- popup.lua +--- +--- Wrapper to make the popup api from vim in neovim. +--- Hope to get this part merged in at some point in the future. +--- +--- Please make sure to update "POPUP.md" with any changes and/or notes. + +local api = vim.api + +local Border = require "neoplen.border" + +local if_nil = vim.F.if_nil + +local clamp = function(value, min, max) + min = min or 0 + max = max or math.huge + + if min then + value = math.max(value, min) + end + if max then + value = math.min(value, max) + end + + return value +end + +local M = {} + +local pos_map = { + topleft = "NW", + topright = "NE", + botleft = "SW", + botright = "SE", +} + +-- Keep track of popup borders, so we don't have to pass them between functions +local borders = {} + +local function dict_default(options, key, default) + if options[key] == nil then + return default[key] + else + return options[key] + end +end + +-- Convert the positional {vim_options} to compatible neovim options and add them to {win_opts} +-- If an option is not given in {vim_options}, fall back to {default_opts} +local function add_position_config(win_opts, vim_options, default_opts) + default_opts = default_opts or {} + + local width = if_nil(vim_options.width, default_opts.width) + local height = if_nil(vim_options.height, default_opts.height) + win_opts.width = clamp(width, vim_options.minwidth, vim_options.maxwidth) + win_opts.height = clamp(height, vim_options.minheight, vim_options.maxheight) + + if vim_options.line and vim_options.line ~= 0 then + win_opts.row = vim_options.line - 1 + else + win_opts.row = math.floor((vim.o.lines - win_opts.height) / 2) + end + + if vim_options.col and vim_options.col ~= 0 then + win_opts.col = vim_options.col - 1 + else + win_opts.col = math.floor((vim.o.columns - win_opts.width) / 2) + end + + -- pos + -- + -- Using "topleft", "topright", "botleft", "botright" defines what corner of the popup "line" + -- and "col" are used for. When not set "topleft" behaviour is used. + -- Alternatively "center" can be used to position the popup in the center of the Neovim window, + -- in which case "line" and "col" are ignored. + if vim_options.pos then + if vim_options.pos == "center" then + vim_options.line = 0 + vim_options.col = 0 + win_opts.anchor = "NW" + else + win_opts.anchor = pos_map[vim_options.pos] + end + else + win_opts.anchor = "NW" -- This is the default, but makes `posinvert` easier to implement + end +end + +function M.create(what, vim_options) + vim_options = vim.deepcopy(vim_options) + + local bufnr + if type(what) == "number" then + bufnr = what + else + bufnr = api.nvim_create_buf(false, true) + assert(bufnr, "Failed to create buffer") + + api.nvim_set_option_value("bufhidden", "wipe", { buf = bufnr }) + api.nvim_set_option_value("modifiable", true, { buf = bufnr }) + + -- TODO: Handle list of lines + if type(what) == "string" then + what = { what } + else + assert(type(what) == "table", '"what" must be a table') + end + + -- padding List with numbers, defining the padding + -- above/right/below/left of the popup (similar to CSS). + -- An empty list uses a padding of 1 all around. The + -- padding goes around the text, inside any border. + -- Padding uses the 'wincolor' highlight. + -- Example: [1, 2, 1, 3] has 1 line of padding above, 2 + -- columns on the right, 1 line below and 3 columns on + -- the left. + if vim_options.padding then + local pad_top, pad_right, pad_below, pad_left + if vim.tbl_isempty(vim_options.padding) then + pad_top = 1 + pad_right = 1 + pad_below = 1 + pad_left = 1 + else + local padding = vim_options.padding + pad_top = padding[1] or 0 + pad_right = padding[2] or 0 + pad_below = padding[3] or 0 + pad_left = padding[4] or 0 + end + + local left_padding = string.rep(" ", pad_left) + local right_padding = string.rep(" ", pad_right) + for index = 1, #what do + what[index] = string.format("%s%s%s", left_padding, what[index], right_padding) + end + + for _ = 1, pad_top do + table.insert(what, 1, "") + end + + for _ = 1, pad_below do + table.insert(what, "") + end + end + + api.nvim_buf_set_lines(bufnr, 0, -1, true, what) + end + + local option_defaults = { + posinvert = true, + zindex = 50, + } + + vim_options.width = if_nil(vim_options.width, 1) + if type(what) == "number" then + vim_options.height = api.nvim_buf_line_count(what) + else + for _, v in ipairs(what) do + vim_options.width = math.max(vim_options.width, #v) + end + vim_options.height = #what + end + + local win_opts = {} + win_opts.relative = "editor" + win_opts.style = "minimal" + win_opts.border = "none" + + -- Add positional and sizing config to win_opts + add_position_config(win_opts, vim_options, { width = 1, height = 1 }) + + -- posinvert, When FALSE the value of "pos" is always used. When + -- , TRUE (the default) and the popup does not fit + -- , vertically and there is more space on the other side + -- , then the popup is placed on the other side of the + -- , position indicated by "line". + if dict_default(vim_options, "posinvert", option_defaults) then + if win_opts.anchor == "NW" or win_opts.anchor == "NE" then + if win_opts.row + win_opts.height > vim.o.lines and win_opts.row * 2 > vim.o.lines then + -- Don't know why, but this is how vim adjusts it + win_opts.row = win_opts.row - win_opts.height - 2 + end + elseif win_opts.anchor == "SW" or win_opts.anchor == "SE" then + if win_opts.row - win_opts.height < 0 and win_opts.row * 2 < vim.o.lines then + -- Don't know why, but this is how vim adjusts it + win_opts.row = win_opts.row + win_opts.height + 2 + end + end + end + + -- textprop, When present the popup is positioned next to a text + -- , property with this name and will move when the text + -- , property moves. Use an empty string to remove. See + -- , |popup-textprop-pos|. + -- related: + -- textpropwin + -- textpropid + + -- zindex, Priority for the popup, default 50. Minimum value is + -- , 1, maximum value is 32000. + local zindex = dict_default(vim_options, "zindex", option_defaults) + win_opts.zindex = clamp(zindex, 1, 32000) + + -- noautocmd, undocumented vim default per https://github.com/vim/vim/issues/5737 + win_opts.noautocmd = if_nil(vim_options.noautocmd, true) + + -- focusable, + -- vim popups are not focusable windows + win_opts.focusable = if_nil(vim_options.focusable, false) + + local win_id = api.nvim_open_win(bufnr, false, win_opts) + + -- Moved, handled after since we need the window ID + if vim_options.moved then + if vim_options.moved == "any" then + vim.lsp.util.close_preview_autocmd({ "CursorMoved", "CursorMovedI" }, win_id) + -- elseif vim_options.moved == "word" then + -- TODO: Handle word, WORD, expr, and the range functions... which seem hard? + end + else + api.nvim_create_autocmd("BufDelete", { + group = api.nvim_create_augroup("neoplen.window", {}), + buffer = bufnr, + once = true, + nested = true, + callback = function() + pcall(api.nvim_win_close, win_id, true) + end, + }) + end + + -- Buffer Options + if vim_options.cursorline then + api.nvim_set_option_value("cursorline", true, { win = win_id }) + end + + if vim_options.wrap ~= nil then + -- set_option wrap should/will trigger autocmd, see https://github.com/neovim/neovim/pull/13247 + if vim_options.noautocmd then + vim.cmd(string.format("noautocmd lua api.nvim_set_option(%s, wrap, %s)", win_id, vim_options.wrap)) + else + api.nvim_set_option_value("wrap", vim_options.wrap, { win = win_id }) + end + end + + -- ===== Not Implemented Options ===== + -- flip: not implemented at the time of writing + -- Mouse: + -- mousemoved: no idea how to do the things with the mouse, so it's an exercise for the reader. + -- drag: mouses are hard + -- resize: mouses are hard + -- close: mouses are hard + -- + -- scrollbar + -- scrollbarhighlight + -- thumbhighlight + + -- tabpage: seems useless + + -- Create border + + local should_show_border = nil + local border_options = {} + + -- border List with numbers, defining the border thickness + -- above/right/below/left of the popup (similar to CSS). + -- Only values of zero and non-zero are recognized. + -- An empty list uses a border all around. + if vim_options.border then + should_show_border = true + + if type(vim_options.border) == "boolean" or vim.tbl_isempty(vim_options.border) then + border_options.border_thickness = Border._default_thickness + elseif #vim_options.border == 4 then + border_options.border_thickness = { + top = clamp(vim_options.border[1], 0, 1), + right = clamp(vim_options.border[2], 0, 1), + bot = clamp(vim_options.border[3], 0, 1), + left = clamp(vim_options.border[4], 0, 1), + } + else + error(string.format("Invalid configuration for border: %s", vim.inspect(vim_options.border))) + end + elseif vim_options.border == false then + should_show_border = false + end + + if (should_show_border == nil or should_show_border) and vim_options.borderchars then + should_show_border = true + + -- borderchars List with characters, defining the character to use + -- for the top/right/bottom/left border. Optionally + -- followed by the character to use for the + -- topleft/topright/botright/botleft corner. + -- Example: ['-', '|', '-', '|', '┌', '┐', '┘', '└'] + -- When the list has one character it is used for all. + -- When the list has two characters the first is used for + -- the border lines, the second for the corners. + -- By default a double line is used all around when + -- 'encoding' is "utf-8" and 'ambiwidth' is "single", + -- otherwise ASCII characters are used. + + local b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft + if vim_options.borderchars == nil then + b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = + "═", "║", "═", "║", "╔", "╗", "╝", "╚" + elseif #vim_options.borderchars == 1 then + local b_char = vim_options.borderchars[1] + b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = + b_char, b_char, b_char, b_char, b_char, b_char, b_char, b_char + elseif #vim_options.borderchars == 2 then + local b_char = vim_options.borderchars[1] + local c_char = vim_options.borderchars[2] + b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = + b_char, b_char, b_char, b_char, c_char, c_char, c_char, c_char + elseif #vim_options.borderchars == 8 then + b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = unpack(vim_options.borderchars) + else + error(string.format 'Not enough arguments for "borderchars"') + end + + border_options.top = b_top + border_options.bot = b_bot + border_options.right = b_right + border_options.left = b_left + border_options.topleft = b_topleft + border_options.topright = b_topright + border_options.botright = b_botright + border_options.botleft = b_botleft + end + + -- title + if vim_options.title then + -- TODO: Check out how title works with weird border combos. + border_options.title = vim_options.title + end + + local border = nil + if should_show_border then + border_options.focusable = vim_options.border_focusable + border_options.highlight = vim_options.borderhighlight and string.format("Normal:%s", vim_options.borderhighlight) + border_options.titlehighlight = vim_options.titlehighlight + border = Border:new(bufnr, win_id, win_opts, border_options) + borders[win_id] = border + end + + if vim_options.highlight then + api.nvim_set_option_value( + "winhl", + string.format("Normal:%s,EndOfBuffer:%s", vim_options.highlight, vim_options.highlight), + { win = win_id } + ) + end + + -- enter + local should_enter = vim_options.enter + if should_enter == nil then + should_enter = true + end + + if should_enter then + -- set focus after border creation so that it's properly placed (especially + -- in relative cursor layout) + if vim_options.noautocmd then + vim.cmd("noautocmd lua api.nvim_set_current_win(" .. win_id .. ")") + else + api.nvim_set_current_win(win_id) + end + end + + -- TODO: Perhaps there's a way to return an object that looks like a window id, + -- but actually has some extra metadata about it. + -- + -- This would make `hidden` a lot easier to manage + return win_id, { + win_id = win_id, + border = border, + } +end + +-- Move popup with window id {win_id} to the position specified with {vim_options}. +-- {vim_options} may contain the following items that determine the popup position/size: +-- - line +-- - col +-- - height +-- - width +-- - maxheight/minheight +-- - maxwidth/minwidth +-- - pos +-- Unimplemented vim options here include: fixed +function M.move(win_id, vim_options) + -- Create win_options + local win_opts = {} + win_opts.relative = "editor" + + local current_pos = api.nvim_win_get_position(win_id) + local default_opts = { + width = api.nvim_win_get_width(win_id), + height = api.nvim_win_get_height(win_id), + row = current_pos[1], + col = current_pos[2], + } + + -- Add positional and sizing config to win_opts + add_position_config(win_opts, vim_options, default_opts) + + -- Update content window + api.nvim_win_set_config(win_id, win_opts) + + -- Update border window (if present) + local border = borders[win_id] + if border ~= nil then + border:move(win_opts, border._border_win_options) + end +end + +return M diff --git a/lua/neoplen/strings.lua b/lua/neoplen/strings.lua new file mode 100644 index 0000000000..4f6ec2f838 --- /dev/null +++ b/lua/neoplen/strings.lua @@ -0,0 +1,98 @@ +local M = {} + +M.strdisplaywidth = vim.fn.strdisplaywidth + +local truncate = function(str, len, dots, direction) + if M.strdisplaywidth(str) <= len then + return str + end + local start = direction > 0 and 0 or str:len() + local current = 0 + local result = "" + local len_of_dots = M.strdisplaywidth(dots) + local concat = function(a, b, dir) + if dir > 0 then + return a .. b + else + return b .. a + end + end + while true do + local part = vim.fn.strcharpart(str, start, 1) + current = current + M.strdisplaywidth(part) + if (current + len_of_dots) > len then + result = concat(result, dots, direction) + break + end + result = concat(result, part, direction) + start = start + direction + end + return result +end + +M.truncate = function(str, len, dots, direction) + str = tostring(str) -- We need to make sure its an actually a string and not a number + dots = dots or "…" + direction = direction or 1 + if direction ~= 0 then + return truncate(str, len, dots, direction) + else + if M.strdisplaywidth(str) <= len then + return str + end + local len1 = math.floor((len + M.strdisplaywidth(dots)) / 2) + local s1 = truncate(str, len1, dots, 1) + local len2 = len - M.strdisplaywidth(s1) + M.strdisplaywidth(dots) + local s2 = truncate(str, len2, dots, -1) + return s1 .. s2:sub(dots:len() + 1) + end +end + +M.align_str = function(string, width, right_justify) + local str_len = M.strdisplaywidth(string) + return right_justify and string.rep(" ", width - str_len) .. string or string .. string.rep(" ", width - str_len) +end + +M.dedent = function(str, leave_indent) + -- Check each line and detect the minimum indent. + local indent + local info = {} + for line in str:gmatch "[^\n]*\n?" do + -- It matches '' for the last line. + if line ~= "" then + local chars, width + local line_indent = line:match "^[ \t]+" + if line_indent then + chars = #line_indent + width = M.strdisplaywidth(line_indent) + if not indent or width < indent then + indent = width + end + -- Ignore empty lines + elseif line ~= "\n" then + indent = 0 + end + table.insert(info, { line = line, chars = chars, width = width }) + end + end + + -- Build up the result + leave_indent = leave_indent or 0 + local result = {} + for _, i in ipairs(info) do + local line + if i.chars then + local content = i.line:sub(i.chars + 1) + local indent_width = i.width - indent + leave_indent + line = (" "):rep(indent_width) .. content + elseif i.line == "\n" then + line = "\n" + else + line = (" "):rep(leave_indent) .. i.line + end + table.insert(result, line) + end + return table.concat(result) +end + +return M diff --git a/lua/telescope/actions/generate.lua b/lua/telescope/actions/generate.lua index 719225a1e0..80e896891c 100644 --- a/lua/telescope/actions/generate.lua +++ b/lua/telescope/actions/generate.lua @@ -24,7 +24,7 @@ local config = require "telescope.config" local action_state = require "telescope.actions.state" local finders = require "telescope.finders" -local action_generate = {} +local M = {} ---@inlinedoc ---@class telescope.actions.generate.which_key.opts @@ -52,14 +52,14 @@ local action_generate = {} --- - Resolves to minimum required number of lines to show hints with `opts` or truncates entries at `max_height`. --- - Closes automatically on action call and can be disabled with by setting `close_with_action` to false. ---@param opts table: options to pass to toggling registered actions -action_generate.which_key = function(opts) +M.which_key = function(opts) local which_key = function(prompt_bufnr) actions.which_key(prompt_bufnr, opts) end return which_key end -action_generate.refine = function(prompt_bufnr, opts) +M.refine = function(prompt_bufnr, opts) opts = opts or {} opts.prompt_to_prefix = vim.F.if_nil(opts.prompt_to_prefix, false) opts.prefix_hl_group = vim.F.if_nil(opts.prompt_hl_group, "TelescopePromptPrefix") @@ -115,4 +115,4 @@ action_generate.refine = function(prompt_bufnr, opts) current_picker:refresh(new_finder, opts) end -return action_generate +return M diff --git a/lua/telescope/actions/history.lua b/lua/telescope/actions/history.lua index d2c6d7a417..f14ba79484 100644 --- a/lua/telescope/actions/history.lua +++ b/lua/telescope/actions/history.lua @@ -1,9 +1,10 @@ +local uv = vim.uv + +local Path = require "neoplen.path" + local conf = require("telescope.config").values -local Path = require "plenary.path" local utils = require "telescope.utils" -local uv = vim.uv - ---@brief --- A base implementation of a prompt history that provides a simple history --- and can be replaced with a custom implementation. @@ -40,7 +41,7 @@ local append_async = function(path, txt) write_async(path, txt, "a") end -local histories = {} +local M = {} --- Manages prompt history ---@class History @Manages prompt history @@ -50,8 +51,8 @@ local histories = {} ---@field content table: History table. Needs to be filled by your own History implementation ---@field index number: Used to keep track of the next or previous index. Default is #content + 1 ---@field cycle_wrap boolean: Controls if history will wrap on reaching beginning or end -histories.History = {} -histories.History.__index = histories.History +M.History = {} +M.History.__index = M.History ---@inlinedoc ---@class telescope.actions.history.opts @@ -62,7 +63,7 @@ histories.History.__index = histories.History --- Create a new History ---@param opts table: Defines the behavior of History -function histories.History:new(opts) +function M.History:new(opts) local obj = {} if conf.history == false or type(conf.history) ~= "table" then obj.enabled = false @@ -86,12 +87,12 @@ function histories.History:new(opts) end --- Shorthand to create a new history -function histories.new(...) - return histories.History:new(...) +function M.new(...) + return M.History:new(...) end --- Will reset the history index to the default initial state. Will happen after the picker closed -function histories.History:reset() +function M.History:reset() if not self.enabled then return end @@ -102,7 +103,7 @@ end ---@param line string: current line that will be appended ---@param picker table: the current picker object ---@param no_reset boolean: On default it will reset the state at the end. If you don't want to do this set to true -function histories.History:append(line, picker, no_reset) +function M.History:append(line, picker, no_reset) if not self.enabled then return end @@ -113,7 +114,7 @@ end ---@param line string: the current line ---@param picker table: the current picker object ---@return string: the next history item -function histories.History:get_next(line, picker) +function M.History:get_next(line, picker) if not self.enabled then utils.notify("History:get_next", { msg = "You are cycling to next the history item but history is disabled. Read ':help telescope.defaults.history'", @@ -142,7 +143,7 @@ end ---@param line string: the current line ---@param picker table: the current picker object ---@return string: the previous history item -function histories.History:get_prev(line, picker) +function M.History:get_prev(line, picker) if not self.enabled then utils.notify("History:get_prev", { msg = "You are cycling to next the history item but history is disabled. Read ':help telescope.defaults.history'", @@ -174,8 +175,8 @@ end --- A simple implementation of history. --- --- It will keep one unified history across all pickers. -histories.get_simple_history = function() - return histories.new { +M.get_simple_history = function() + return M.new { init = function(obj) local p = Path:new(obj.path) if not p:exists() then @@ -213,4 +214,4 @@ histories.get_simple_history = function() } end -return histories +return M diff --git a/lua/telescope/actions/init.lua b/lua/telescope/actions/init.lua index 7e8eb5ba36..1021df9815 100644 --- a/lua/telescope/actions/init.lua +++ b/lua/telescope/actions/init.lua @@ -45,15 +45,16 @@ --- Another interesting thing to do is that these actions now have functions you --- can call. These functions include `:replace(f)`, `:replace_if(f, c)`, --- `replace_map(tbl)` and `enhance(tbl)`. More information on these functions ---- can be found in the `developers.md` and `lua/tests/automated/action_spec.lua` +--- can be found in the `developers.md` and `tests/automated/action_spec.lua` --- file. local api = vim.api +local popup = require "neoplen.popup" + local conf = require("telescope.config").values local state = require "telescope.state" local utils = require "telescope.utils" -local popup = require "plenary.popup" local p_scroller = require "telescope.pickers.scroller" local action_state = require "telescope.actions.state" diff --git a/lua/telescope/actions/set.lua b/lua/telescope/actions/set.lua index d362d99c29..a799ff025c 100644 --- a/lua/telescope/actions/set.lua +++ b/lua/telescope/actions/set.lua @@ -10,7 +10,6 @@ local api = vim.api local log = require "telescope.log" -local Path = require "plenary.path" local state = require "telescope.state" local utils = require "telescope.utils" @@ -192,7 +191,7 @@ action_set.edit = function(prompt_bufnr, command) -- check if we didn't pick a different buffer -- prevents restarting lsp server if api.nvim_buf_get_name(0) ~= filename or command ~= "edit" then - filename = Path:new(filename):normalize(vim.uv.cwd()) + filename = vim.fs.normalize(filename) pcall(vim.cmd, string.format("%s %s", command, vim.fn.fnameescape(filename))) end end diff --git a/lua/telescope/actions/utils.lua b/lua/telescope/actions/utils.lua index 96b523a116..79a57fea2d 100644 --- a/lua/telescope/actions/utils.lua +++ b/lua/telescope/actions/utils.lua @@ -102,7 +102,7 @@ end -- Best effort to infer function names for actions.which_key function utils._get_anon_function_name(info) - local Path = require "plenary.path" + local Path = require "neoplen.path" local fname -- if fn defined in string (ie loadstring) source is string -- if fn defined in file, source is file name prefixed with a `@´ diff --git a/lua/telescope/algos/fzy.lua b/lua/telescope/algos/fzy.lua index bf322ab437..37e0c456fb 100644 --- a/lua/telescope/algos/fzy.lua +++ b/lua/telescope/algos/fzy.lua @@ -7,14 +7,7 @@ -- > matches on consecutive letters and starts of words. This allows matching -- > using acronyms or different parts of the path." - J Hawthorn -local has_path, Path = pcall(require, "plenary.path") -if not has_path then - Path = { - path = { - separator = "/", - }, - } -end +local pathsep = vim.fn.has "win32" == 1 and "\\" or "/" local SCORE_GAP_LEADING = -0.005 local SCORE_GAP_TRAILING = -0.005 @@ -58,10 +51,10 @@ end local function precompute_bonus(haystack) local match_bonus = {} - local last_char = Path.path.sep + local last_char = pathsep for i = 1, string.len(haystack) do local this_char = haystack:sub(i, i) - if last_char == Path.path.sep then + if last_char == pathsep then match_bonus[i] = SCORE_MATCH_SLASH elseif last_char == "-" or last_char == "_" or last_char == " " then match_bonus[i] = SCORE_MATCH_WORD diff --git a/lua/telescope/async_job.lua b/lua/telescope/async_job.lua index 742dc8e81e..e87cc2d983 100644 --- a/lua/telescope/async_job.lua +++ b/lua/telescope/async_job.lua @@ -1,10 +1,10 @@ local uv = vim.uv -local Object = require "plenary.class" -local log = require "plenary.log" +local Object = require "neoplen.class" +local log = require "neoplen.log" -local async = require "plenary.async" -local channel = require("plenary.async").control.channel +local async = require "neoplen.async" +local channel = require("neoplen.async").control.channel local utils = require "telescope.utils" local M = {} diff --git a/lua/telescope/builtin/__files.lua b/lua/telescope/builtin/__files.lua index e179be8ae3..61019da6a8 100644 --- a/lua/telescope/builtin/__files.lua +++ b/lua/telescope/builtin/__files.lua @@ -12,10 +12,7 @@ local utils = require "telescope.utils" local conf = require("telescope.config").values local log = require "telescope.log" -local Path = require "plenary.path" - local flatten = utils.flatten -local filter = vim.tbl_filter local files = {} @@ -63,7 +60,7 @@ local get_open_filelist = function(grep_open_files, cwd) return nil end - local bufnrs = filter(function(b) + local bufnrs = vim.tbl_filter(function(b) if 1 ~= vim.fn.buflisted(b) then return false end @@ -76,7 +73,7 @@ local get_open_filelist = function(grep_open_files, cwd) local filelist = {} for _, bufnr in ipairs(bufnrs) do local file = api.nvim_buf_get_name(bufnr) - table.insert(filelist, Path:new(file):make_relative(cwd)) + table.insert(filelist, vim.fs.relpath(cwd, file)) end return filelist end diff --git a/lua/telescope/builtin/__git.lua b/lua/telescope/builtin/__git.lua index d19d7e362f..788b3346ef 100644 --- a/lua/telescope/builtin/__git.lua +++ b/lua/telescope/builtin/__git.lua @@ -1,5 +1,8 @@ local api = vim.api +local strings = require "neoplen.strings" +local Path = require "neoplen.path" + local actions = require "telescope.actions" local action_state = require "telescope.actions.state" local finders = require "telescope.finders" @@ -9,8 +12,6 @@ local pickers = require "telescope.pickers" local previewers = require "telescope.previewers" local utils = require "telescope.utils" local entry_display = require "telescope.pickers.entry_display" -local strings = require "plenary.strings" -local Path = require "plenary.path" local conf = require("telescope.config").values local git_command = utils.__git_command @@ -466,11 +467,11 @@ local try_worktrees = function(opts) end local current_path_toplevel = function() - local gitdir = vim.fn.finddir(".git", vim.fn.expand "%:p" .. ";") + local gitdir = vim.fn.finddir(".git", vim.fn.expand "%:p" .. ";") --[[@as string]] if gitdir == "" then return nil end - return Path:new(gitdir):parent():absolute() + return Path:new(vim.fs.dirname(gitdir)):absolute() end local set_opts_cwd = function(opts) diff --git a/lua/telescope/builtin/__internal.lua b/lua/telescope/builtin/__internal.lua index 67787b0c0c..d0f369a503 100644 --- a/lua/telescope/builtin/__internal.lua +++ b/lua/telescope/builtin/__internal.lua @@ -1,11 +1,12 @@ local api = vim.api +local Path = require "neoplen.path" + local actions = require "telescope.actions" local action_set = require "telescope.actions.set" local action_state = require "telescope.actions.state" local finders = require "telescope.finders" local make_entry = require "telescope.make_entry" -local Path = require "plenary.path" local pickers = require "telescope.pickers" local previewers = require "telescope.previewers" local p_window = require "telescope.pickers.window" @@ -30,8 +31,9 @@ end ---@return boolean local function buf_in_cwd(bufname, cwd) - if cwd:sub(-1) ~= Path.path.sep then - cwd = cwd .. Path.path.sep + local os_sep = utils.get_separator() + if cwd:sub(-1) ~= os_sep then + cwd = cwd .. os_sep end local bufname_prefix = bufname:sub(1, #cwd) return bufname_prefix == cwd @@ -241,7 +243,7 @@ internal.planets = function(opts) local show_pluto = opts.show_pluto or false local show_moon = opts.show_moon or false - local sourced_file = require("plenary.debug_utils").sourced_filepath() + local sourced_file = debug.getinfo(2, "S").source:sub(2) local base_directory = vim.fn.fnamemodify(sourced_file, ":h:h:h:h") local globbed_files = vim.fn.globpath(base_directory .. "/data/memes/planets/", "*", true, true) @@ -925,7 +927,7 @@ internal.reloader = function(opts) end actions.close(prompt_bufnr) - require("plenary.reload").reload_module(selection.value) + require("neoplen.reload").reload_module(selection.value) utils.notify("builtin.reloader", { msg = string.format("[%s] - module reloaded", selection.value), level = "INFO", diff --git a/lua/telescope/builtin/__lsp.lua b/lua/telescope/builtin/__lsp.lua index a9b99cc292..b253b03647 100644 --- a/lua/telescope/builtin/__lsp.lua +++ b/lua/telescope/builtin/__lsp.lua @@ -3,7 +3,8 @@ local api = vim.api local lsp = vim.lsp -local channel = require("plenary.async.control").channel +local channel = require("neoplen.async.control").channel + local actions = require "telescope.actions" local sorters = require "telescope.sorters" local conf = require("telescope.config").values diff --git a/lua/telescope/config.lua b/lua/telescope/config.lua index 81588915a6..d83b659b4c 100644 --- a/lua/telescope/config.lua +++ b/lua/telescope/config.lua @@ -1,7 +1,7 @@ -local strings = require "plenary.strings" +local strings = require "neoplen.strings" + local sorters = require "telescope.sorters" -local os_sep = require("plenary.path").path.sep -local has_win = vim.fn.has "win32" == 1 +local iswin = vim.fn.has "win32" == 1 -- Keep the values around between reloads _TelescopeConfigurationValues = _TelescopeConfigurationValues or {} @@ -519,7 +519,7 @@ append( append( "history", { - path = vim.fn.stdpath "data" .. os_sep .. "telescope_history", + path = vim.fs.joinpath(vim.fn.stdpath "data", "telescope_history"), limit = 100, handler = function(...) return require("telescope.actions.history").get_simple_history(...) @@ -598,7 +598,7 @@ append( append( "preview", { - check_mime_type = not has_win, + check_mime_type = not iswin, filesize_limit = 25, highlight_limit = 1, timeout = 250, @@ -665,7 +665,7 @@ append( end, -- 2) Truncate lines to preview window for too large files filesize_hook = function(filepath, bufnr, opts) - local path = require("plenary.path"):new(filepath) + local path = require("neoplen.path"):new(filepath) -- opts exposes winid local height = vim.api.nvim_win_get_height(opts.winid) local lines = vim.split(path:head(height), "[\r]?\n") diff --git a/lua/telescope/finders.lua b/lua/telescope/finders.lua index c230a5801e..4b97d90957 100644 --- a/lua/telescope/finders.lua +++ b/lua/telescope/finders.lua @@ -1,4 +1,4 @@ -local Job = require "plenary.job" +local Job = require "neoplen.job" local make_entry = require "telescope.make_entry" local log = require "telescope.log" diff --git a/lua/telescope/finders/async_oneshot_finder.lua b/lua/telescope/finders/async_oneshot_finder.lua index fb94af2d72..e0fc637c17 100644 --- a/lua/telescope/finders/async_oneshot_finder.lua +++ b/lua/telescope/finders/async_oneshot_finder.lua @@ -1,4 +1,5 @@ -local async = require "plenary.async" +local async = require "neoplen.async" + local async_job = require "telescope.async_job" local LinesPipe = async_job.LinesPipe diff --git a/lua/telescope/finders/async_static_finder.lua b/lua/telescope/finders/async_static_finder.lua index 39b8eb99a0..8ffd4adac3 100644 --- a/lua/telescope/finders/async_static_finder.lua +++ b/lua/telescope/finders/async_static_finder.lua @@ -1,4 +1,4 @@ -local scheduler = require("plenary.async").util.scheduler +local scheduler = require("neoplen.async").util.scheduler local make_entry = require "telescope.make_entry" diff --git a/lua/telescope/health.lua b/lua/telescope/health.lua index 0862331d96..a08117381c 100644 --- a/lua/telescope/health.lua +++ b/lua/telescope/health.lua @@ -2,7 +2,8 @@ local health = vim.health local extension_module = require "telescope._extensions" local extension_info = require("telescope").extensions -local is_win = vim.api.nvim_call_function("has", { "win32" }) == 1 + +local iswin = vim.fn.has "win32" == 1 local optional_dependencies = { { @@ -29,14 +30,14 @@ local optional_dependencies = { } local required_plugins = { - { lib = "plenary", optional = false }, + { lib = "neoplen", optional = false }, } local check_binary_installed = function(package) local binaries = package.binaries or { package.name } for _, binary in ipairs(binaries) do local found = vim.fn.executable(binary) == 1 - if not found and is_win then + if not found and iswin then binary = binary .. ".exe" found = vim.fn.executable(binary) == 1 end diff --git a/lua/telescope/log.lua b/lua/telescope/log.lua index c74378f078..e00a744038 100644 --- a/lua/telescope/log.lua +++ b/lua/telescope/log.lua @@ -1,4 +1,4 @@ -return require("plenary.log").new { +return require("neoplen.log").new { plugin = "telescope", level = "info", } diff --git a/lua/telescope/make_entry.lua b/lua/telescope/make_entry.lua index 387b87798d..a340f56198 100644 --- a/lua/telescope/make_entry.lua +++ b/lua/telescope/make_entry.lua @@ -33,10 +33,11 @@ local api = vim.api +local strings = require "neoplen.strings" +local Path = require "neoplen.path" + local entry_display = require "telescope.pickers.entry_display" local utils = require "telescope.utils" -local strings = require "plenary.strings" -local Path = require "plenary.path" local treesitter_type_highlight = { ["associated"] = "TSConstant", @@ -278,7 +279,7 @@ do local execute_keys = { path = function(t) - if Path:new(t.filename):is_absolute() then + if vim.fn.isabsolutepath(t.filename) then return t.filename, false else return Path:new({ t.cwd, t.filename }):absolute(), false @@ -1104,7 +1105,7 @@ function make_entry.gen_from_ctags(opts) end local kind = string.match(extension_fields or "", "kind:(%S+)") - if Path.path.sep == "\\" then + if utils.get_separator() == "\\" then file = string.gsub(file, "/", "\\") end diff --git a/lua/telescope/pickers.lua b/lua/telescope/pickers.lua index 4e0058f513..58083494f2 100644 --- a/lua/telescope/pickers.lua +++ b/lua/telescope/pickers.lua @@ -2,10 +2,12 @@ require "telescope" local api = vim.api -local async = require "plenary.async" +local async = require "neoplen.async" local await_schedule = async.util.scheduler -local channel = require("plenary.async.control").channel -local popup = require "plenary.popup" +local channel = require("neoplen.async.control").channel +local popup = require "neoplen.popup" +local truncate = require("neoplen.strings").truncate +local strdisplaywidth = require("neoplen.strings").strdisplaywidth local actions = require "telescope.actions" local config = require "telescope.config" @@ -24,9 +26,6 @@ local Layout = require "telescope.pickers.layout" local EntryManager = require "telescope.entry_manager" local MultiSelect = require "telescope.pickers.multi" -local truncate = require("plenary.strings").truncate -local strdisplaywidth = require("plenary.strings").strdisplaywidth - local ns_telescope_matching = api.nvim_create_namespace "telescope_matching" local ns_telescope_prompt = api.nvim_create_namespace "telescope_prompt" local ns_telescope_prompt_prefix = api.nvim_create_namespace "telescope_prompt_prefix" diff --git a/lua/telescope/pickers/entry_display.lua b/lua/telescope/pickers/entry_display.lua index 8767a39007..d2e83a6ee4 100644 --- a/lua/telescope/pickers/entry_display.lua +++ b/lua/telescope/pickers/entry_display.lua @@ -55,7 +55,8 @@ --- For a better understanding of how create() and displayer are used it's best to look --- at the code in make_entry.lua. -local strings = require "plenary.strings" +local strings = require "neoplen.strings" + local state = require "telescope.state" local resolve = require "telescope.config.resolve" diff --git a/lua/telescope/pickers/highlights.lua b/lua/telescope/pickers/highlights.lua index ab16b57228..e410de9283 100644 --- a/lua/telescope/pickers/highlights.lua +++ b/lua/telescope/pickers/highlights.lua @@ -3,7 +3,7 @@ local hl = vim.hl local log = require "telescope.log" local conf = require("telescope.config").values -local highlights = {} +local M = {} local ns_telescope_selection = api.nvim_create_namespace "telescope_selection" local ns_telescope_multiselection = api.nvim_create_namespace "telescope_multiselection" @@ -117,8 +117,8 @@ function Highlighter:hi_multiselect(row, is_selected) end end -highlights.new = function(...) +M.new = function(...) return Highlighter:new(...) end -return highlights +return M diff --git a/lua/telescope/pickers/scroller.lua b/lua/telescope/pickers/scroller.lua index a658f68507..bcf3b4ccc5 100644 --- a/lua/telescope/pickers/scroller.lua +++ b/lua/telescope/pickers/scroller.lua @@ -1,4 +1,4 @@ -local scroller = {} +local M = {} local range_calculators = { ascending = function(max_results, num_results) @@ -40,7 +40,7 @@ local scroll_calculators = { end, } -scroller.create = function(scroll_strategy, sorting_strategy) +M.create = function(scroll_strategy, sorting_strategy) local range_fn = range_calculators[sorting_strategy] if not range_fn then error(debug.traceback("Unknown sorting strategy: " .. sorting_strategy)) @@ -83,14 +83,14 @@ scroller.create = function(scroll_strategy, sorting_strategy) end end -scroller.top = function(sorting_strategy, max_results, num_results) +M.top = function(sorting_strategy, max_results, num_results) if sorting_strategy == "ascending" then return 0 end return (num_results > max_results) and 0 or (max_results - num_results) end -scroller.middle = function(sorting_strategy, max_results, num_results) +M.middle = function(sorting_strategy, max_results, num_results) local mid_pos if sorting_strategy == "ascending" then @@ -102,14 +102,14 @@ scroller.middle = function(sorting_strategy, max_results, num_results) return (num_results < max_results) and mid_pos or math.floor(max_results / 2) end -scroller.bottom = function(sorting_strategy, max_results, num_results) +M.bottom = function(sorting_strategy, max_results, num_results) if sorting_strategy == "ascending" then return math.min(max_results, num_results) - 1 end return max_results - 1 end -scroller.better = function(sorting_strategy) +M.better = function(sorting_strategy) if sorting_strategy == "ascending" then return -1 else @@ -117,8 +117,8 @@ scroller.better = function(sorting_strategy) end end -scroller.worse = function(sorting_strategy) - return -(scroller.better(sorting_strategy)) +M.worse = function(sorting_strategy) + return -(M.better(sorting_strategy)) end -return scroller +return M diff --git a/lua/telescope/pickers/window.lua b/lua/telescope/pickers/window.lua index 945a7e3a40..e85ae86232 100644 --- a/lua/telescope/pickers/window.lua +++ b/lua/telescope/pickers/window.lua @@ -1,8 +1,8 @@ local resolve = require "telescope.config.resolve" -local p_window = {} +local M = {} -function p_window.get_window_options(picker, max_columns, max_lines) +function M.get_window_options(picker, max_columns, max_lines) local layout_strategy = picker.layout_strategy local getter = require("telescope.pickers.layout_strategies")[layout_strategy] @@ -13,7 +13,7 @@ function p_window.get_window_options(picker, max_columns, max_lines) return getter(picker, max_columns, max_lines) end -function p_window.get_initial_window_options(picker) +function M.get_initial_window_options(picker) local popup_border = resolve.win_option(picker.window.border) local popup_borderchars = resolve.win_option(picker.window.borderchars) @@ -46,4 +46,4 @@ function p_window.get_initial_window_options(picker) } end -return p_window +return M diff --git a/lua/telescope/previewers/buffer_previewer.lua b/lua/telescope/previewers/buffer_previewer.lua index 66bba3c5fe..87b95c3544 100644 --- a/lua/telescope/previewers/buffer_previewer.lua +++ b/lua/telescope/previewers/buffer_previewer.lua @@ -1,8 +1,9 @@ local api = vim.api local hl = vim.hl +local Path = require "neoplen.path" + local from_entry = require "telescope.from_entry" -local Path = require "plenary.path" local utils = require "telescope.utils" local putils = require "telescope.previewers.utils" local Previewer = require "telescope.previewers.previewer" @@ -78,88 +79,6 @@ local function split(s, sep, plain, opts) end local bytes_to_megabytes = math.pow(1024, 2) -local color_hash = { - ["p"] = "TelescopePreviewPipe", - ["c"] = "TelescopePreviewCharDev", - ["d"] = "TelescopePreviewDirectory", - ["b"] = "TelescopePreviewBlock", - ["l"] = "TelescopePreviewLink", - ["s"] = "TelescopePreviewSocket", - ["."] = "TelescopePreviewNormal", - ["r"] = "TelescopePreviewRead", - ["w"] = "TelescopePreviewWrite", - ["x"] = "TelescopePreviewExecute", - ["-"] = "TelescopePreviewHyphen", - ["T"] = "TelescopePreviewSticky", - ["S"] = "TelescopePreviewSticky", - [2] = "TelescopePreviewSize", - [3] = "TelescopePreviewUser", - [4] = "TelescopePreviewGroup", - [5] = "TelescopePreviewDate", -} -color_hash[6] = function(line) - return color_hash[line:sub(1, 1)] -end - -local colorize_ls_long = function(bufnr, data, sections) - local windows_add = Path.path.sep == "\\" and 2 or 0 - for lnum, line in ipairs(data) do - local section = sections[lnum] - for i = 1, section[1].end_index - 1 do -- Highlight permissions - local c = line:sub(i, i) - hl.range(bufnr, ns_previewer, color_hash[c], { lnum - 1, i - 1 }, { lnum - 1, i }) - end - for i = 2, #section do -- highlights size, (user, group), date and name - local hl_group = color_hash[i + (i ~= 2 and windows_add or 0)] - hl.range( - bufnr, - ns_previewer, - type(hl_group) == "function" and hl_group(line) or hl_group, - { lnum - 1, section[i].start_index - 1 }, - { lnum - 1, section[i].end_index - 1 } - ) - end - end -end - -local handle_directory_preview = function(filepath, bufnr, opts) - opts.preview.ls_short = vim.F.if_nil(opts.preview.ls_short, false) - - local set_colorize_lines - if opts.preview.ls_short then - set_colorize_lines = function(data, sections) - local PATH_SECTION = Path.path.sep == "\\" and 4 or 6 - local paths = {} - for i, line in ipairs(data) do - local section = sections[i][PATH_SECTION] - local path = line:sub(section.start_index, section.end_index) - table.insert(paths, path) - end - api.nvim_buf_set_lines(bufnr, 0, -1, false, paths) - for i, path in ipairs(paths) do - local hlgroup = color_hash[6](data[i]) - hl.range(bufnr, ns_previewer, hlgroup, { i - 1, 0 }, { i - 1, #path }) - end - end - else - set_colorize_lines = function(data, sections) - api.nvim_buf_set_lines(bufnr, 0, -1, false, data) - colorize_ls_long(bufnr, data, sections) - end - end - - require("plenary.scandir").ls_async(filepath, { - hidden = true, - group_directories_first = true, - on_exit = vim.schedule_wrap(function(data, sections) - set_colorize_lines(data, sections) - if opts.callback then - opts.callback(bufnr) - end - end), - }) -end - local handle_file_preview = function(filepath, bufnr, stat, opts) vim.schedule(function() opts.ft = opts.use_ft_detect and putils.filetype_detect(filepath) @@ -270,9 +189,7 @@ previewers.file_maker = function(filepath, bufnr, opts) if not stat then return end - if stat.type == "directory" then - handle_directory_preview(filepath, bufnr, opts) - else + if stat.type ~= "directory" then handle_file_preview(filepath, bufnr, stat, opts) end end) diff --git a/lua/telescope/previewers/term_previewer.lua b/lua/telescope/previewers/term_previewer.lua index 4826bd848a..82c45b8367 100644 --- a/lua/telescope/previewers/term_previewer.lua +++ b/lua/telescope/previewers/term_previewer.lua @@ -1,15 +1,16 @@ local api = vim.api +local Path = require "neoplen.path" + local conf = require("telescope.config").values local utils = require "telescope.utils" -local Path = require "plenary.path" local from_entry = require "telescope.from_entry" local Previewer = require "telescope.previewers.previewer" local putil = require "telescope.previewers.utils" local defaulter = utils.make_default_callable -local previewers = {} +local M = {} -- TODO: Should play with these some more, ty @clason local bat_options = { "--style=plain", "--color=always", "--paging=always" } @@ -104,7 +105,7 @@ local get_maker = function(opts) return maker end -previewers.new_termopen_previewer = function(opts) +M.new_termopen_previewer = function(opts) opts = opts or {} assert(opts.get_command, "get_command is a required function") @@ -246,13 +247,13 @@ previewers.new_termopen_previewer = function(opts) return Previewer:new(opts) end -previewers.cat = defaulter(function(opts) +M.cat = defaulter(function(opts) opts = opts or {} local maker = get_maker(opts) local cwd = opts.cwd or vim.uv.cwd() - return previewers.new_termopen_previewer { + return M.new_termopen_previewer { title = "File Preview", dyn_title = function(_, entry) return Path:new(from_entry.path(entry, false, false)):normalize(cwd) @@ -269,13 +270,13 @@ previewers.cat = defaulter(function(opts) } end, {}) -previewers.vimgrep = defaulter(function(opts) +M.vimgrep = defaulter(function(opts) opts = opts or {} local maker = get_maker(opts) local cwd = opts.cwd or vim.uv.cwd() - return previewers.new_termopen_previewer { + return M.new_termopen_previewer { title = "Grep Preview", dyn_title = function(_, entry) return Path:new(from_entry.path(entry, false, false)):normalize(cwd) @@ -304,13 +305,13 @@ previewers.vimgrep = defaulter(function(opts) } end, {}) -previewers.qflist = defaulter(function(opts) +M.qflist = defaulter(function(opts) opts = opts or {} local maker = get_maker(opts) local cwd = opts.cwd or vim.uv.cwd() - return previewers.new_termopen_previewer { + return M.new_termopen_previewer { title = "Grep Preview", dyn_title = function(_, entry) return Path:new(from_entry.path(entry, false, false)):normalize(cwd) @@ -341,4 +342,4 @@ previewers.qflist = defaulter(function(opts) } end, {}) -return previewers +return M diff --git a/lua/telescope/previewers/utils.lua b/lua/telescope/previewers/utils.lua index d1bfa5fe36..06c20377cb 100644 --- a/lua/telescope/previewers/utils.lua +++ b/lua/telescope/previewers/utils.lua @@ -1,66 +1,20 @@ local api = vim.api -local ts_utils = require "telescope.utils" -local strings = require "plenary.strings" -local conf = require("telescope.config").values - -local Job = require "plenary.job" -local Path = require "plenary.path" - -local utils = {} - -local detect_from_shebang = function(p) - local s = p:readbyterange(0, 256) - if s then - local lines = ts_utils.split_lines(s) - return vim.filetype.match { contents = lines } - end -end - -local parse_modeline = function(tail) - if tail:find "vim:" then - return tail:match ".*:ft=([^: ]*):.*$" or "" - end -end - -local detect_from_modeline = function(p) - local s = p:readbyterange(-256, 256) - if s then - local lines = ts_utils.split_lines(s) - local idx = lines[#lines] ~= "" and #lines or #lines - 1 - if idx >= 1 then - return parse_modeline(lines[idx]) - end - end -end - -utils.filetype_detect = function(filepath) - if type(filepath) ~= string then - filepath = tostring(filepath) - end +local Job = require "neoplen.job" +local strings = require "neoplen.strings" - local match = vim.filetype.match { filename = filepath } - if match and match ~= "" then - return match - end +local utils = require "telescope.utils" +local conf = require("telescope.config").values - local p = Path:new(filepath) - if p and p:is_file() then - match = detect_from_shebang(p) - if match and match ~= "" then - return match - end +local M = {} - match = detect_from_modeline(p) - if match and match ~= "" then - return match - end - end +M.filetype_detect = function(filepath) + return vim.filetype.match { filename = filepath } end -- API helper functions for buffer previewer --- Job maker for buffer previewer -utils.job_maker = function(cmd, bufnr, opts) +M.job_maker = function(cmd, bufnr, opts) opts = opts or {} opts.mode = opts.mode or "insert" -- bufname and value are optional @@ -112,7 +66,7 @@ local function has_filetype(ft) end --- Attach default highlighter which will choose between regex and ts -utils.highlighter = function(bufnr, ft, opts) +M.highlighter = function(bufnr, ft, opts) opts = vim.F.if_nil(opts, {}) opts.preview = vim.F.if_nil(opts.preview, {}) opts.preview.treesitter = (function() @@ -151,15 +105,15 @@ utils.highlighter = function(bufnr, ft, opts) local ts_success if ts_highlighting then - ts_success = utils.ts_highlighter(bufnr, ft) + ts_success = M.ts_highlighter(bufnr, ft) end if not ts_highlighting or ts_success == false then - utils.regex_highlighter(bufnr, ft) + M.regex_highlighter(bufnr, ft) end end --- Attach regex highlighter -utils.regex_highlighter = function(bufnr, ft) +M.regex_highlighter = function(bufnr, ft) if has_filetype(ft) then return pcall(api.nvim_set_option_value, "syntax", ft, { buf = bufnr }) end @@ -167,7 +121,7 @@ utils.regex_highlighter = function(bufnr, ft) end -- Attach ts highlighter -utils.ts_highlighter = function(bufnr, ft) +M.ts_highlighter = function(bufnr, ft) if has_filetype(ft) then local lang = vim.treesitter.language.get_lang(ft) if lang and vim.treesitter.language.add(lang) then @@ -177,7 +131,7 @@ utils.ts_highlighter = function(bufnr, ft) return false end -utils.set_preview_message = function(bufnr, winid, message, fillchar) +M.set_preview_message = function(bufnr, winid, message, fillchar) fillchar = vim.F.if_nil(fillchar, "╱") local height = api.nvim_win_get_height(winid) local width = api.nvim_win_get_width(winid) @@ -186,10 +140,10 @@ utils.set_preview_message = function(bufnr, winid, message, fillchar) 0, -1, false, - ts_utils.repeated_table(height, table.concat(ts_utils.repeated_table(width, fillchar), "")) + utils.repeated_table(height, table.concat(utils.repeated_table(width, fillchar), "")) ) local anon_ns = api.nvim_create_namespace "" - local padding = table.concat(ts_utils.repeated_table(#message + 4, " "), "") + local padding = table.concat(utils.repeated_table(#message + 4, " "), "") local formatted_message = " " .. message .. " " -- Populate lines table based on height local lines = {} @@ -224,7 +178,7 @@ end --- info. ---@param mime_type string ---@return boolean -utils.binary_mime_type = function(mime_type) +M.binary_mime_type = function(mime_type) local type_, subtype = unpack(vim.split(mime_type, "/")) if vim.tbl_contains({ "text", "inode" }, type_) then return false @@ -235,4 +189,4 @@ utils.binary_mime_type = function(mime_type) return true end -return utils +return M diff --git a/lua/telescope/testharness/helpers.lua b/lua/telescope/testharness/helpers.lua index 1f0b0b1d78..af70667055 100644 --- a/lua/telescope/testharness/helpers.lua +++ b/lua/telescope/testharness/helpers.lua @@ -1,30 +1,30 @@ -local test_helpers = {} +local M = {} -test_helpers.get_picker = function() +M.get_picker = function() local state = require "telescope.state" return state.get_status(vim.api.nvim_get_current_buf()).picker end -test_helpers.get_results_bufnr = function() +M.get_results_bufnr = function() local state = require "telescope.state" return state.get_status(vim.api.nvim_get_current_buf()).layout.results.bufnr end -test_helpers.get_file = function() +M.get_file = function() return vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ":t") end -test_helpers.get_prompt = function() +M.get_prompt = function() return vim.api.nvim_buf_get_lines(0, 0, -1, false)[1] end -test_helpers.get_results = function() - return vim.api.nvim_buf_get_lines(test_helpers.get_results_bufnr(), 0, -1, false) +M.get_results = function() + return vim.api.nvim_buf_get_lines(M.get_results_bufnr(), 0, -1, false) end -test_helpers.get_best_result = function() - local results = test_helpers.get_results() - local picker = test_helpers.get_picker() +M.get_best_result = function() + local results = M.get_results() + local picker = M.get_picker() if picker.sorting_strategy == "ascending" then return results[1] @@ -33,24 +33,24 @@ test_helpers.get_best_result = function() end end -test_helpers.get_selection = function() +M.get_selection = function() local state = require "telescope.state" return state.get_global_key "selected_entry" end -test_helpers.get_selection_value = function() - return test_helpers.get_selection().value +M.get_selection_value = function() + return M.get_selection().value end -test_helpers.make_globals = function() - GetFile = test_helpers.get_file -- luacheck: globals GetFile - GetPrompt = test_helpers.get_prompt -- luacheck: globals GetPrompt +M.make_globals = function() + GetFile = M.get_file -- luacheck: globals GetFile + GetPrompt = M.get_prompt -- luacheck: globals GetPrompt - GetResults = test_helpers.get_results -- luacheck: globals GetResults - GetBestResult = test_helpers.get_best_result -- luacheck: globals GetBestResult + GetResults = M.get_results -- luacheck: globals GetResults + GetBestResult = M.get_best_result -- luacheck: globals GetBestResult - GetSelection = test_helpers.get_selection -- luacheck: globals GetSelection - GetSelectionValue = test_helpers.get_selection_value -- luacheck: globals GetSelectionValue + GetSelection = M.get_selection -- luacheck: globals GetSelection + GetSelectionValue = M.get_selection_value -- luacheck: globals GetSelectionValue end -return test_helpers +return M diff --git a/lua/telescope/testharness/init.lua b/lua/telescope/testharness/init.lua index 9a8e707388..b1e48149f6 100644 --- a/lua/telescope/testharness/init.lua +++ b/lua/telescope/testharness/init.lua @@ -1,13 +1,11 @@ local assert = require "luassert" -local Path = require "plenary.path" - local tester = {} tester.debug = false local get_results_from_contents = function(content) local nvim = vim.fn.jobstart( - { "nvim", "--noplugin", "-u", "scripts/minimal_init.vim", "--headless", "--embed" }, + { "nvim", "--headless", "--embed", "--clean", "-u", "scripts/minimal_init.lua" }, { rpc = true } ) @@ -78,10 +76,9 @@ tester.run_string = function(contents) end tester.run_file = function(filename) - local file = "./lua/tests/pickers/" .. filename .. ".lua" - local path = Path:new(file) + local file = "./tests/pickers/" .. filename .. ".lua" - if not path:exists() then + if not vim.uv.fs_stat(file) then assert.are.same("", file) end @@ -98,7 +95,7 @@ tester.run_file = function(filename) return {ok, msg or runner.state} end)() ]], - path:absolute() + vim.fs.abspath(file) ) check_results(get_results_from_contents(contents)) diff --git a/lua/telescope/testharness/runner.lua b/lua/telescope/testharness/runner.lua index af1bf30bd4..abbcd34aa5 100644 --- a/lua/telescope/testharness/runner.lua +++ b/lua/telescope/testharness/runner.lua @@ -1,17 +1,17 @@ local builtin = require "telescope.builtin" local DELAY = vim.g.telescope_test_delay or 50 -local runner = {} +local M = {} -- State is test variable -runner.state = { +M.state = { done = false, results = {}, msgs = {}, } local writer = function(val) - table.insert(runner.state.results, val) + table.insert(M.state.results, val) end local invalid_test_case = function(k) @@ -27,14 +27,14 @@ local replace_terms = function(input) return vim.api.nvim_replace_termcodes(input, true, false, true) end -runner.nvim_feed = function(text, feed_opts) +M.nvim_feed = function(text, feed_opts) feed_opts = feed_opts or "m" vim.api.nvim_feedkeys(text, feed_opts, true) end local end_test_cases = function() - runner.state.done = true + M.state.done = true end local execute_test_case = function(location, key, spec) @@ -65,11 +65,11 @@ local execute_test_case = function(location, key, spec) return ok end -runner.log = function(msg) - table.insert(runner.state.msgs, msg) +M.log = function(msg) + table.insert(M.state.msgs, msg) end -runner.picker = function(picker_name, input, test_cases, opts) +M.picker = function(picker_name, input, test_cases, opts) opts = opts or {} for k, _ in pairs(test_cases) do @@ -79,11 +79,11 @@ runner.picker = function(picker_name, input, test_cases, opts) end opts.on_complete = { - runner.create_on_complete(input, test_cases), + M.create_on_complete(input, test_cases), } opts._on_error = function(self, msg) - runner.state.done = true + M.state.done = true writer { location = "Error while running on complete", expected = "To Work", @@ -91,12 +91,12 @@ runner.picker = function(picker_name, input, test_cases, opts) } end - runner.log "Starting picker" + M.log "Starting picker" builtin[picker_name](opts) - runner.log "Called picker" + M.log "Called picker" end -runner.create_on_complete = function(input, test_cases) +M.create_on_complete = function(input, test_cases) input = replace_terms(input) local actions = {} @@ -104,8 +104,8 @@ runner.create_on_complete = function(input, test_cases) local char = input:sub(i, i) table.insert(actions, { cb = function() - runner.log("Inserting char: " .. char) - runner.nvim_feed(char, "") + M.log("Inserting char: " .. char) + M.nvim_feed(char, "") end, char = char, }) @@ -135,7 +135,7 @@ runner.create_on_complete = function(input, test_cases) end vim.defer_fn(function() - runner.nvim_feed(replace_terms "", "") + M.nvim_feed(replace_terms "", "") vim.defer_fn(function() if test_cases.post_close then @@ -153,4 +153,4 @@ runner.create_on_complete = function(input, test_cases) end end -return runner +return M diff --git a/lua/telescope/utils.lua b/lua/telescope/utils.lua index d773a9a487..24f2817077 100644 --- a/lua/telescope/utils.lua +++ b/lua/telescope/utils.lua @@ -3,17 +3,15 @@ local api = vim.api -local Path = require "plenary.path" -local Job = require "plenary.job" - +local Job = require "neoplen.job" local log = require "telescope.log" +local truncate = require("neoplen.strings").truncate -local truncate = require("plenary.strings").truncate local get_status = require("telescope.state").get_status local utils = {} -utils.iswin = vim.uv.os_uname().sysname == "Windows_NT" +utils.iswin = vim.fn.has "win32" == 1 ---@param t table ---@return table @@ -71,7 +69,7 @@ utils.path_expand = function(path) end utils.get_separator = function() - return Path.path.sep + return utils.iswin and "\\" or "/" end utils.cycle = function(i, n) @@ -201,15 +199,55 @@ local path_truncate = function(path, truncate_len, opts) return truncate(path, opts.__length - opts.__prefix, nil, -1) end -local path_shorten = function(path, length, exclude) - if exclude ~= nil then - return Path:new(path):shorten(length, exclude) - else - return Path:new(path):shorten(length) +local path_shorten = function(filename, len, exclude) + len = len or 1 + exclude = exclude or { -1 } + local exc = {} + local pathsep = utils.get_separator() + + -- get parts in a table + local parts = {} + local empty_pos = {} + for m in (filename .. pathsep):gmatch("(.-)" .. pathsep) do + if m ~= "" then + parts[#parts + 1] = m + else + table.insert(empty_pos, #parts + 1) + end + end + + for _, v in pairs(exclude) do + if v < 0 then + exc[v + #parts + 1] = true + else + exc[v] = true + end end + + local final_path_components = {} + local count = 1 + for _, match in ipairs(parts) do + if not exc[count] and #match > len then + table.insert(final_path_components, string.sub(match, 1, len)) + else + table.insert(final_path_components, match) + end + table.insert(final_path_components, pathsep) + count = count + 1 + end + + local l = #final_path_components -- so that we don't need to keep calculating length + table.remove(final_path_components, l) -- remove final slash + + -- add back empty positions + for i = #empty_pos, 1, -1 do + table.insert(final_path_components, empty_pos[i], pathsep) + end + + return table.concat(final_path_components) end -local path_abs = function(path, opts) +local path_rel = function(path, opts) local cwd if opts.cwd then cwd = opts.cwd @@ -219,7 +257,7 @@ local path_abs = function(path, opts) else cwd = vim.uv.cwd() end - return Path:new(path):make_relative(cwd) + return vim.fs.relpath(cwd, path) or path end -- IMPORTANT: This function should have been a local function as it's only used @@ -365,7 +403,7 @@ utils.transform_path = function(opts, path) end if not vim.tbl_contains(path_display, "absolute") and not path_display.absolute then - transformed_path = path_abs(transformed_path, opts) + transformed_path = path_rel(transformed_path, opts) end if vim.tbl_contains(path_display, "smart") or path_display.smart then @@ -518,16 +556,6 @@ function utils.max_split(s, pattern, maxsplit) return t end --- IMPORTANT: This function should have been a local function as it's only used --- in this file, but the code was already exported a long time ago. By making it --- local we would potential break consumers of this method. -function utils.data_directory() - local sourced_file = require("plenary.debug_utils").sourced_filepath() - local base_directory = vim.fn.fnamemodify(sourced_file, ":h:h:h") - - return Path:new({ base_directory, "data" }):absolute() .. Path.path.sep -end - function utils.buffer_dir() return vim.fn.expand "%:p:h" end diff --git a/scripts/minimal_init.lua b/scripts/minimal_init.lua new file mode 100644 index 0000000000..afd6e4f2eb --- /dev/null +++ b/scripts/minimal_init.lua @@ -0,0 +1,5 @@ +vim.o.rtp = os.getenv "PLENTEST" .. ",.," .. vim.o.rtp +vim.o.rtp = ",../tree-sitter-lua," .. vim.o.rtp + +vim.cmd.runtime { "plugin/telescope", bang = true } +vim.g.telescope_test_delay = 100 diff --git a/scripts/minimal_init.vim b/scripts/minimal_init.vim deleted file mode 100644 index 39870eb94e..0000000000 --- a/scripts/minimal_init.vim +++ /dev/null @@ -1,7 +0,0 @@ -set rtp+=. -set rtp+=../plenary.nvim/ - -runtime! plugin/plenary.vim -runtime! plugin/telescope.lua - -let g:telescope_test_delay = 100 diff --git a/telescope.nvim-scm-1.rockspec b/telescope.nvim-scm-1.rockspec index 73e29261e3..10dd9c3ab3 100644 --- a/telescope.nvim-scm-1.rockspec +++ b/telescope.nvim-scm-1.rockspec @@ -17,7 +17,6 @@ description = { dependencies = { 'lua == 5.1', - 'plenary.nvim', } source = { diff --git a/lua/tests/automated/action_spec.lua b/tests/automated/action_spec.lua similarity index 100% rename from lua/tests/automated/action_spec.lua rename to tests/automated/action_spec.lua diff --git a/lua/tests/automated/command_spec.lua b/tests/automated/command_spec.lua similarity index 100% rename from lua/tests/automated/command_spec.lua rename to tests/automated/command_spec.lua diff --git a/lua/tests/automated/entry_display_spec.lua b/tests/automated/entry_display_spec.lua similarity index 100% rename from lua/tests/automated/entry_display_spec.lua rename to tests/automated/entry_display_spec.lua diff --git a/lua/tests/automated/entry_manager_spec.lua b/tests/automated/entry_manager_spec.lua similarity index 100% rename from lua/tests/automated/entry_manager_spec.lua rename to tests/automated/entry_manager_spec.lua diff --git a/lua/tests/automated/layout_strategies_spec.lua b/tests/automated/layout_strategies_spec.lua similarity index 100% rename from lua/tests/automated/layout_strategies_spec.lua rename to tests/automated/layout_strategies_spec.lua diff --git a/lua/tests/automated/linked_list_spec.lua b/tests/automated/linked_list_spec.lua similarity index 100% rename from lua/tests/automated/linked_list_spec.lua rename to tests/automated/linked_list_spec.lua diff --git a/lua/tests/automated/pickers/find_files_spec.lua b/tests/automated/pickers/find_files_spec.lua similarity index 91% rename from lua/tests/automated/pickers/find_files_spec.lua rename to tests/automated/pickers/find_files_spec.lua index 5a1c460b16..7bf80f7b86 100644 --- a/lua/tests/automated/pickers/find_files_spec.lua +++ b/tests/automated/pickers/find_files_spec.lua @@ -1,5 +1,4 @@ --- Just skip on mac, it has flaky CI for some reason -if vim.fn.has "mac" == 1 or require("telescope.utils").iswin then +if require("telescope.utils").iswin then return end @@ -115,8 +114,8 @@ describe("builtin.find_files", function() post_typed = { { { - " lua/tests/fixtures/find_files/file_a.txt", - "> lua/tests/fixtures/find_files/file_abc.txt", + " tests/fixtures/find_files/file_a.txt", + "> tests/fixtures/find_files/file_abc.txt", }, GetResults }, }, @@ -135,7 +134,7 @@ describe("builtin.find_files", function() tester.run_string [[ runner.picker('find_files', 'fixtures/find_files/file_abc', { post_typed = { - { 'lua/tests/fixtures/find_files/file_abc.txt', GetSelectionValue }, + { 'tests/fixtures/find_files/file_abc.txt', GetSelectionValue }, } }) ]] diff --git a/lua/tests/automated/pickers/live_grep_spec.lua b/tests/automated/pickers/live_grep_spec.lua similarity index 90% rename from lua/tests/automated/pickers/live_grep_spec.lua rename to tests/automated/pickers/live_grep_spec.lua index 3471c9062f..bef19ee715 100644 --- a/lua/tests/automated/pickers/live_grep_spec.lua +++ b/tests/automated/pickers/live_grep_spec.lua @@ -1,4 +1,4 @@ -if vim.fn.has "mac" == 1 or require("telescope.utils").iswin then +if require("telescope.utils").iswin then return end @@ -34,7 +34,7 @@ describe("builtin.live_grep", function() vim.tbl_extend("force", { sorter = require("telescope.sorters").get_fzy_sorter(), layout_strategy = "center", - cwd = "./lua/tests/fixtures/live_grep", + cwd = "./tests/fixtures/live_grep", temp__scrolling_limit = 5, }, vim.json.decode [==[%s]==]) ) diff --git a/lua/tests/automated/resolver_spec.lua b/tests/automated/resolver_spec.lua similarity index 100% rename from lua/tests/automated/resolver_spec.lua rename to tests/automated/resolver_spec.lua diff --git a/lua/tests/automated/scroller_spec.lua b/tests/automated/scroller_spec.lua similarity index 100% rename from lua/tests/automated/scroller_spec.lua rename to tests/automated/scroller_spec.lua diff --git a/lua/tests/automated/sorters_spec.lua b/tests/automated/sorters_spec.lua similarity index 100% rename from lua/tests/automated/sorters_spec.lua rename to tests/automated/sorters_spec.lua diff --git a/lua/tests/automated/telescope_spec.lua b/tests/automated/telescope_spec.lua similarity index 99% rename from lua/tests/automated/telescope_spec.lua rename to tests/automated/telescope_spec.lua index 44594dd3fa..9548e687b7 100644 --- a/lua/tests/automated/telescope_spec.lua +++ b/tests/automated/telescope_spec.lua @@ -1,5 +1,5 @@ +local Path = require "neoplen.path" local picker = require "telescope.pickers" -local Path = require "plenary.path" local eq = assert.are.same diff --git a/lua/tests/automated/utils_spec.lua b/tests/automated/utils_spec.lua similarity index 99% rename from lua/tests/automated/utils_spec.lua rename to tests/automated/utils_spec.lua index a03a758d86..887ba32fe8 100644 --- a/lua/tests/automated/utils_spec.lua +++ b/tests/automated/utils_spec.lua @@ -1,4 +1,4 @@ -local Path = require "plenary.path" +local Path = require "neoplen.path" local utils = require "telescope.utils" local eq = assert.are.equal diff --git a/lua/tests/fixtures/find_files/file_a.txt b/tests/fixtures/find_files/file_a.txt similarity index 100% rename from lua/tests/fixtures/find_files/file_a.txt rename to tests/fixtures/find_files/file_a.txt diff --git a/lua/tests/fixtures/find_files/file_abc.txt b/tests/fixtures/find_files/file_abc.txt similarity index 100% rename from lua/tests/fixtures/find_files/file_abc.txt rename to tests/fixtures/find_files/file_abc.txt diff --git a/lua/tests/fixtures/live_grep/a.txt b/tests/fixtures/live_grep/a.txt similarity index 100% rename from lua/tests/fixtures/live_grep/a.txt rename to tests/fixtures/live_grep/a.txt diff --git a/lua/tests/helpers.lua b/tests/helpers.lua similarity index 100% rename from lua/tests/helpers.lua rename to tests/helpers.lua diff --git a/lua/tests/pickers/find_files__readme.lua b/tests/pickers/find_files__readme.lua similarity index 100% rename from lua/tests/pickers/find_files__readme.lua rename to tests/pickers/find_files__readme.lua diff --git a/lua/tests/pickers/find_files__scrolling_descending_cycle.lua b/tests/pickers/find_files__scrolling_descending_cycle.lua similarity index 100% rename from lua/tests/pickers/find_files__scrolling_descending_cycle.lua rename to tests/pickers/find_files__scrolling_descending_cycle.lua