├── .luacheckrc ├── lua └── ide │ ├── lib │ ├── gh │ │ ├── client_test.lua │ │ └── client.lua │ ├── options.lua │ ├── workspace.lua │ ├── web.lua │ ├── git │ │ └── client_test.lua │ ├── autocmd.lua │ ├── encoding │ │ └── base64.lua │ ├── commands.lua │ ├── async_client_test.lua │ ├── popup.lua │ ├── win.lua │ ├── sort.lua │ ├── buf.lua │ ├── lsp.lua │ └── async_client.lua │ ├── workspaces │ ├── notifiers │ │ └── git.lua │ ├── workspace_controller_test.lua │ ├── workspace_registry.lua │ ├── workspace_test.lua │ ├── commands.lua │ └── component_tracker.lua │ ├── trees │ ├── lib.lua │ ├── depth_table.lua │ ├── node.lua │ ├── tree_test.lua │ ├── marshaller.lua │ └── tree.lua │ ├── icons │ ├── init.lua │ ├── nerd_icon_set.lua │ ├── codicon_icon_set.lua │ └── icon_set.lua │ ├── buffers │ ├── doomscrollbuffer_test.lua │ ├── buffer_test.lua │ ├── diffbuffer_test.lua │ ├── doomscrollbuffer.lua │ ├── buffer.lua │ └── diffbuffer.lua │ ├── components │ ├── changes │ │ ├── init.lua │ │ ├── statusnode.lua │ │ └── commands.lua │ ├── commits │ │ ├── init.lua │ │ ├── commands.lua │ │ └── commitnode.lua │ ├── outline │ │ ├── init.lua │ │ ├── commands.lua │ │ └── symbolnode.lua │ ├── branches │ │ ├── init.lua │ │ ├── commands.lua │ │ └── branchnode.lua │ ├── terminal │ │ ├── init.lua │ │ ├── terminalbrowser │ │ │ ├── init.lua │ │ │ ├── terminalnode.lua │ │ │ ├── commands.lua │ │ │ └── component.lua │ │ ├── commands.lua │ │ └── component.lua │ ├── timeline │ │ ├── init.lua │ │ ├── timelinenode.lua │ │ └── commands.lua │ ├── bufferlist │ │ ├── init.lua │ │ ├── commands.lua │ │ └── component.lua │ ├── callhierarchy │ │ ├── init.lua │ │ ├── commands.lua │ │ └── callnode.lua │ ├── bookmarks │ │ ├── notebook_test.lua │ │ ├── init.lua │ │ ├── commands.lua │ │ ├── bookmarknode.lua │ │ └── notebook.lua │ └── explorer │ │ ├── init.lua │ │ ├── presets.lua │ │ ├── prompts.lua │ │ └── commands.lua │ ├── logger │ ├── logger_test.lua │ └── logger.lua │ ├── panels │ ├── test_component.lua │ ├── component_factory.lua │ ├── panel_registry_test.lua │ ├── panel_registry.lua │ └── panel_test.lua │ ├── init.lua │ └── config.lua ├── .github └── FUNDING.yml ├── contrib └── screenshot.png ├── LICENSE ├── doc └── tags └── README.md /.luacheckrc: -------------------------------------------------------------------------------- 1 | globals = { "vim" } 2 | -------------------------------------------------------------------------------- /lua/ide/lib/gh/client_test.lua: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lua/ide/workspaces/notifiers/git.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ldelossa] 4 | -------------------------------------------------------------------------------- /contrib/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ldelossa/nvim-ide/HEAD/contrib/screenshot.png -------------------------------------------------------------------------------- /lua/ide/trees/lib.lua: -------------------------------------------------------------------------------- 1 | local Lib = {} 2 | 3 | -- Builds a tree from a list of filesystem paths. 4 | -- 5 | -- All paths will be children of the given root. 6 | function Lib.tree_from_paths(root, paths) end 7 | 8 | return Lib 9 | -------------------------------------------------------------------------------- /lua/ide/icons/init.lua: -------------------------------------------------------------------------------- 1 | local Icons = {} 2 | 3 | -- Set the global IconSet to the default IconSet. 4 | -- This can be overwritten later to change the icon set used across all components 5 | -- which use this global. 6 | Icons.global_icon_set = require("ide.icons.codicon_icon_set").new() 7 | 8 | return Icons 9 | -------------------------------------------------------------------------------- /lua/ide/lib/options.lua: -------------------------------------------------------------------------------- 1 | local Options = {} 2 | 3 | function Options.merge(default, provided) 4 | if provided == nil then 5 | provided = {} 6 | end 7 | if default == nil then 8 | default = {} 9 | end 10 | return vim.tbl_deep_extend("force", default, provided) 11 | end 12 | 13 | return Options 14 | -------------------------------------------------------------------------------- /lua/ide/lib/workspace.lua: -------------------------------------------------------------------------------- 1 | local Workspace = {} 2 | 3 | -- Given a @Workspace, determine if nvim-ide is currently opened to it. 4 | function Workspace.is_current_ws(ws) 5 | if ws.tab == vim.api.nvim_get_current_tabpage() then 6 | return true 7 | end 8 | return false 9 | end 10 | 11 | return Workspace 12 | -------------------------------------------------------------------------------- /lua/ide/lib/gh/client.lua: -------------------------------------------------------------------------------- 1 | local async_client = require('ide.lib.async_client') 2 | 3 | local GH = {} 4 | 5 | GH.RECORD_SEP = '␞' 6 | GH.GROUP_SEP = '␝' 7 | 8 | GH.new = function() 9 | local self = async_client.new("gh") 10 | 11 | local function handle_req(req) 12 | 13 | end 14 | end 15 | 16 | return GH 17 | -------------------------------------------------------------------------------- /lua/ide/lib/web.lua: -------------------------------------------------------------------------------- 1 | local Web = {} 2 | 3 | -- Opens the `url` with either `xdg-open` for linux or `open` for macOS. 4 | -- 5 | -- @url - @string, an http(s) protocol url. 6 | function Web.open_link(url) 7 | if vim.fn.has("mac") == 1 then 8 | vim.fn.system({ "open", url }) 9 | else 10 | vim.fn.system({ "xdg-open", url }) 11 | end 12 | end 13 | 14 | return Web 15 | -------------------------------------------------------------------------------- /lua/ide/buffers/doomscrollbuffer_test.lua: -------------------------------------------------------------------------------- 1 | local dsbuf = require("ide.buffers.doomscrollbuffer") 2 | 3 | local M = {} 4 | 5 | function M.test_functionality() 6 | local buf = dsbuf.new(function(_) 7 | return { "additional line 1", "additional line 2" } 8 | end) 9 | print(vim.inspect(buf)) 10 | buf.write_lines({ "first" }) 11 | buf.write_lines({ "second" }) 12 | buf.write_lines({ "third" }) 13 | end 14 | 15 | return M 16 | -------------------------------------------------------------------------------- /lua/ide/buffers/buffer_test.lua: -------------------------------------------------------------------------------- 1 | local buffer = require("ide.buffers.buffer") 2 | local M = {} 3 | 4 | function M.test_functionality() 5 | local buf = buffer.new(false, false) 6 | buf.write_lines({ "first" }) 7 | buf.write_lines({ "second" }) 8 | buf.write_lines({ "third" }) 9 | 10 | local lines = buf.read_lines() 11 | assert(lines[1] == "first") 12 | assert(lines[2] == "second") 13 | assert(lines[3] == "third") 14 | end 15 | 16 | return M 17 | -------------------------------------------------------------------------------- /lua/ide/components/changes/init.lua: -------------------------------------------------------------------------------- 1 | local component_factory = require("ide.panels.component_factory") 2 | local component = require("ide.components.changes.component") 3 | 4 | local Init = {} 5 | 6 | Init.Name = "Changes" 7 | 8 | local function register_component() 9 | component_factory.register(Init.Name, component.new) 10 | end 11 | 12 | -- call yourself, this will be triggered when another module wants to reference 13 | -- ide.components.explorer.Name 14 | register_component() 15 | 16 | return Init 17 | -------------------------------------------------------------------------------- /lua/ide/components/commits/init.lua: -------------------------------------------------------------------------------- 1 | local component_factory = require("ide.panels.component_factory") 2 | local component = require("ide.components.commits.component") 3 | 4 | local Init = {} 5 | 6 | Init.Name = "Commits" 7 | 8 | local function register_component() 9 | component_factory.register(Init.Name, component.new) 10 | end 11 | 12 | -- call yourself, this will be triggered when another module wants to reference 13 | -- ide.components.explorer.Name 14 | register_component() 15 | 16 | return Init 17 | -------------------------------------------------------------------------------- /lua/ide/components/outline/init.lua: -------------------------------------------------------------------------------- 1 | local component_factory = require("ide.panels.component_factory") 2 | local component = require("ide.components.outline.component") 3 | 4 | local Init = {} 5 | 6 | Init.Name = "Outline" 7 | 8 | local function register_component() 9 | component_factory.register(Init.Name, component.new) 10 | end 11 | 12 | -- call yourself, this will be triggered when another module wants to reference 13 | -- ide.components.explorer.Name 14 | register_component() 15 | 16 | return Init 17 | -------------------------------------------------------------------------------- /lua/ide/components/branches/init.lua: -------------------------------------------------------------------------------- 1 | local component_factory = require("ide.panels.component_factory") 2 | local component = require("ide.components.branches.component") 3 | 4 | local Init = {} 5 | 6 | Init.Name = "Branches" 7 | 8 | local function register_component() 9 | component_factory.register(Init.Name, component.new) 10 | end 11 | 12 | -- call yourself, this will be triggered when another module wants to reference 13 | -- ide.components.explorer.Name 14 | register_component() 15 | 16 | return Init 17 | -------------------------------------------------------------------------------- /lua/ide/components/terminal/init.lua: -------------------------------------------------------------------------------- 1 | local component_factory = require("ide.panels.component_factory") 2 | local component = require("ide.components.terminal.component") 3 | 4 | local Init = {} 5 | 6 | Init.Name = "Terminal" 7 | 8 | local function register_component() 9 | component_factory.register(Init.Name, component.new) 10 | end 11 | 12 | -- call yourself, this will be triggered when another module wants to reference 13 | -- ide.components.explorer.Name 14 | register_component() 15 | 16 | return Init 17 | -------------------------------------------------------------------------------- /lua/ide/components/timeline/init.lua: -------------------------------------------------------------------------------- 1 | local component_factory = require("ide.panels.component_factory") 2 | local component = require("ide.components.timeline.component") 3 | 4 | local Init = {} 5 | 6 | Init.Name = "Timeline" 7 | 8 | local function register_component() 9 | component_factory.register(Init.Name, component.new) 10 | end 11 | 12 | -- call yourself, this will be triggered when another module wants to reference 13 | -- ide.components.explorer.Name 14 | register_component() 15 | 16 | return Init 17 | -------------------------------------------------------------------------------- /lua/ide/components/bufferlist/init.lua: -------------------------------------------------------------------------------- 1 | local component_factory = require("ide.panels.component_factory") 2 | local component = require("ide.components.bufferlist.component") 3 | 4 | local Init = {} 5 | 6 | Init.Name = "BufferList" 7 | 8 | local function register_component() 9 | component_factory.register(Init.Name, component.new) 10 | end 11 | 12 | -- call yourself, this will be triggered when another module wants to reference 13 | -- ide.components.bufferlist.Name 14 | register_component() 15 | 16 | return Init 17 | -------------------------------------------------------------------------------- /lua/ide/logger/logger_test.lua: -------------------------------------------------------------------------------- 1 | local logger = require("ide.logger.logger") 2 | 3 | local M = {} 4 | 5 | function M.test_functionality() 6 | local log = logger.new("test", "test_functionality") 7 | log.error("test error log %s %s", "test-value", "test-value2") 8 | log.info("test info log %s %s", "test-value", "test-value2") 9 | log.warning("test warning log %s %s", "test-value", "test-value2") 10 | log.debug("test debug log %s %s", "test-value", "test-value2") 11 | logger.open_log() 12 | end 13 | 14 | return M 15 | -------------------------------------------------------------------------------- /lua/ide/components/callhierarchy/init.lua: -------------------------------------------------------------------------------- 1 | local component_factory = require("ide.panels.component_factory") 2 | local component = require("ide.components.callhierarchy.component") 3 | 4 | local Init = {} 5 | 6 | Init.Name = "CallHierarchy" 7 | 8 | local function register_component() 9 | component_factory.register(Init.Name, component.new) 10 | end 11 | 12 | -- call yourself, this will be triggered when another module wants to reference 13 | -- ide.components.explorer.Name 14 | register_component() 15 | 16 | return Init 17 | -------------------------------------------------------------------------------- /lua/ide/workspaces/workspace_controller_test.lua: -------------------------------------------------------------------------------- 1 | local workspace_ctlr = require("ide.workspaces.workspace_controller") 2 | local component_factory = require("ide.panels.component_factory") 3 | local test_component = require("ide.panels.test_component") 4 | 5 | local M = {} 6 | 7 | function M.test_functionality() 8 | -- register a component with the component factory 9 | component_factory.register("test-component", test_component.new) 10 | 11 | local wsc = workspace_ctlr.new() 12 | wsc.init() 13 | end 14 | 15 | return M 16 | -------------------------------------------------------------------------------- /lua/ide/components/bookmarks/notebook_test.lua: -------------------------------------------------------------------------------- 1 | local notebook = require("ide.components.bookmarks.notebook") 2 | 3 | local M = {} 4 | 5 | function M.test_functionality() 6 | vim.fn.mkdir("/tmp/notebook-test") 7 | vim.fn.writefile({}, "/tmp/notebook-test/test nb.notebook") 8 | 9 | local buf = vim.api.nvim_create_buf(true, false) 10 | 11 | local nb = notebook.new(buf, "test nb", "/tmp/notebook-test/test nb.notebook") 12 | nb.create_bookmark({}) 13 | 14 | -- vim.fn.delete("/tmp/notebook-test", "rf") 15 | end 16 | 17 | return M 18 | -------------------------------------------------------------------------------- /lua/ide/components/terminal/terminalbrowser/init.lua: -------------------------------------------------------------------------------- 1 | local component_factory = require("ide.panels.component_factory") 2 | local component = require("ide.components.terminal.terminalbrowser.component") 3 | 4 | local Init = {} 5 | 6 | Init.Name = "TerminalBrowser" 7 | 8 | local function register_component() 9 | component_factory.register(Init.Name, component.new) 10 | end 11 | 12 | -- call yourself, this will be triggered when another module wants to reference 13 | -- ide.components.explorer.Name 14 | register_component() 15 | 16 | return Init 17 | -------------------------------------------------------------------------------- /lua/ide/components/bookmarks/init.lua: -------------------------------------------------------------------------------- 1 | local component_factory = require("ide.panels.component_factory") 2 | local component = require("ide.components.bookmarks.component") 3 | 4 | local Init = {} 5 | 6 | Init.Name = "Bookmarks" 7 | 8 | local function register_component() 9 | component_factory.register(Init.Name, component.new) 10 | 11 | if vim.fn.isdirectory(vim.fn.fnamemodify(component.NotebooksPath, ":p")) == 0 then 12 | vim.fn.mkdir(vim.fn.fnamemodify(component.NotebooksPath, ":p")) 13 | end 14 | end 15 | 16 | -- call yourself, this will be triggered when another module wants to reference 17 | -- ide.components.explorer.Name 18 | register_component() 19 | 20 | return Init 21 | -------------------------------------------------------------------------------- /lua/ide/panels/test_component.lua: -------------------------------------------------------------------------------- 1 | local component = require("ide.panels.component") 2 | 3 | -- TestComponent is a derived Component which simply creates a buffer with its 4 | -- name. 5 | -- 6 | -- Used for testing purposes. 7 | local TestComponent = {} 8 | 9 | TestComponent.new = function(name) 10 | local self = component.new(name) 11 | 12 | -- implementation open which returns a simple buffer with a 13 | -- string. 14 | function self.open() 15 | local buf = vim.api.nvim_create_buf(false, true) 16 | vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "test component: " .. self.name }) 17 | return buf 18 | end 19 | 20 | -- no-op for post_win_create() 21 | function self.post_win_create() end 22 | 23 | return self 24 | end 25 | 26 | return TestComponent 27 | -------------------------------------------------------------------------------- /lua/ide/components/terminal/terminalbrowser/terminalnode.lua: -------------------------------------------------------------------------------- 1 | local node = require("ide.trees.node") 2 | local icons = require("ide.icons") 3 | 4 | local TerminalNode = {} 5 | 6 | TerminalNode.new = function(id, name, depth) 7 | -- extends 'ide.trees.Node' fields. 8 | 9 | local self = node.new("terminal", name, id, depth) 10 | self.id = id 11 | 12 | -- Marshal a symbolnode into a buffer line. 13 | -- 14 | -- @return: @icon - @string, icon for symbol's kind 15 | -- @name - @string, symbol's name 16 | -- @details - @string, symbol's detail if exists. 17 | function self.marshal() 18 | local icon = icons.global_icon_set.get_icon("Terminal") 19 | local name = self.name 20 | local detail = "" 21 | return icon, name, detail 22 | end 23 | 24 | return self 25 | end 26 | 27 | return TerminalNode 28 | -------------------------------------------------------------------------------- /lua/ide/components/explorer/init.lua: -------------------------------------------------------------------------------- 1 | local component_factory = require("ide.panels.component_factory") 2 | local component = require("ide.components.explorer.component") 3 | 4 | local Init = {} 5 | 6 | Init.Name = "Explorer" 7 | 8 | local function register_component() 9 | if pcall(require, "nvim-web-devicons") then 10 | -- setup the dir icon and file type. 11 | local devicons = require("nvim-web-devicons") 12 | require("nvim-web-devicons").set_icon({ 13 | ["dir"] = { 14 | icon = "", 15 | color = "#6d8086", 16 | cterm_color = "108", 17 | name = "Directory", 18 | }, 19 | }) 20 | devicons.set_up_highlights() 21 | end 22 | 23 | component_factory.register(Init.Name, component.new) 24 | end 25 | 26 | -- call yourself, this will be triggered when another module wants to reference 27 | -- ide.components.explorer.Name 28 | register_component() 29 | 30 | return Init 31 | -------------------------------------------------------------------------------- /lua/ide/init.lua: -------------------------------------------------------------------------------- 1 | local config = require("ide.config") 2 | local logger = require("ide.logger.logger") 3 | 4 | local M = {} 5 | 6 | function M.setup(user_config) 7 | -- merge the incoming config with our global, forcing the right table's keys 8 | -- to overwrite the left's. 9 | config.config = vim.tbl_deep_extend("force", config.config, (user_config or {})) 10 | 11 | -- configure the global icon set. 12 | if config.config["icon_set"] == "nerd" then 13 | require("ide.icons").global_icon_set = require("ide.icons.nerd_icon_set").new() 14 | end 15 | if config.config["icon_set"] == "codicon" then 16 | require("ide.icons").global_icon_set = require("ide.icons.codicon_icon_set").new() 17 | end 18 | 19 | -- set the global log level of the any logger. 20 | logger.set_log_level(config.config.log_level) 21 | 22 | -- create and launch a workspace controller. 23 | local wsctrl = require("ide.workspaces.workspace_controller").new() 24 | wsctrl.init() 25 | end 26 | 27 | return M 28 | -------------------------------------------------------------------------------- /lua/ide/lib/git/client_test.lua: -------------------------------------------------------------------------------- 1 | local git = require("ide.lib.git.client") 2 | 3 | local M = {} 4 | 5 | function M.test_functionality() 6 | local client = git.new() 7 | -- client.log_file_history( 8 | -- "lua/ide/lib/buf.lua", 9 | -- 0, 10 | -- 10, 11 | -- function(commits) 12 | -- print(vim.inspect(commits)) 13 | -- end 14 | -- ) 15 | -- client.log( 16 | -- "HEAD", 17 | -- 1, 18 | -- function(commits) 19 | -- print(vim.inspect(commits)) 20 | -- end 21 | -- ) 22 | -- client.status(function(stats) 23 | -- print(vim.inspect(stats)) 24 | -- end) 25 | -- client.show_rev_paths("HEAD", function(paths) 26 | -- print(vim.inspect(paths)) 27 | -- end) 28 | -- client.log_commits(0, 10, function(commits) 29 | -- print(vim.inspect(commits)) 30 | -- end) 31 | -- print(vim.inspect(git.compare_sha("abcde", "abcd"))) 32 | -- print(vim.inspect(git.compare_sha("abcde", "abcdg"))) 33 | client.head(function(rev) 34 | print(rev) 35 | end) 36 | end 37 | 38 | return M 39 | -------------------------------------------------------------------------------- /lua/ide/components/explorer/presets.lua: -------------------------------------------------------------------------------- 1 | local Presets = {} 2 | 3 | Presets.default = { 4 | change_dir = "cd", 5 | close = "X", 6 | collapse = "zc", 7 | collapse_all = "zM", 8 | copy_file = "p", 9 | delete_file = "D", 10 | deselect_file = "", 11 | edit = "", 12 | edit_split = "s", 13 | edit_tab = "t", 14 | edit_vsplit = "v", 15 | expand = "zo", 16 | file_details = "i", 17 | help = "?", 18 | hide = "H", 19 | maximize = "+", 20 | minimize = "-", 21 | move_file = "m", 22 | new_dir = "d", 23 | new_file = "n", 24 | rename_file = "r", 25 | select_file = "", 26 | toggle_exec_perm = "*", 27 | up_dir = "..", 28 | } 29 | 30 | Presets.nvim_tree = vim.tbl_deep_extend("force", Presets.default, { 31 | -- new_file can be used to create direcotries 32 | -- by just ending with a `/`, like in nvim_tree 33 | collapse_all = "W", 34 | delete_file = "d", 35 | edit_split = "", 36 | edit_tab = "", 37 | edit_vsplit = "", 38 | hide = "", 39 | new_dir = "", 40 | new_file = "a", 41 | }) 42 | 43 | return Presets 44 | -------------------------------------------------------------------------------- /lua/ide/lib/autocmd.lua: -------------------------------------------------------------------------------- 1 | local AutoCMD = {} 2 | 3 | -- Creates a pair of autocommands. 4 | -- The `on_enter` callback is called when `buf` is entered. 5 | -- Likewise the `on_leave` callback is issued the buffer is unfocused. 6 | -- 7 | -- This is useful when you want to use a noisy autocmd event such as "CursorMoved" 8 | -- but only when inside a specific buffer. 9 | -- 10 | -- Both autocmds will be deleted once `buf` is deleted. 11 | function AutoCMD.buf_enter_and_leave(buf, on_enter, on_leave) 12 | local buf_enter = vim.api.nvim_create_autocmd({ "BufEnter" }, { 13 | buffer = buf, 14 | callback = function(args) 15 | on_enter(args) 16 | end, 17 | }) 18 | 19 | local buf_leave = vim.api.nvim_create_autocmd({ "BufLeave" }, { 20 | buffer = buf, 21 | callback = function(args) 22 | on_leave(args) 23 | end, 24 | }) 25 | 26 | vim.api.nvim_create_autocmd({ "BufDelete" }, { 27 | buffer = buf, 28 | callback = function() 29 | vim.api.nvim_del_autocmd(buf_enter) 30 | vim.api.nvim_del_autocmd(buf_leave) 31 | end, 32 | }) 33 | end 34 | 35 | return AutoCMD 36 | -------------------------------------------------------------------------------- /lua/ide/panels/component_factory.lua: -------------------------------------------------------------------------------- 1 | -- ComponentFactory registers @Component's constructor functions. 2 | -- This allows uncoupled modules to request the creation of known @Component(s) 3 | -- 4 | -- This is a global singleton resource that other modules can use after import. 5 | local ComponentFactory = {} 6 | 7 | local factory = {} 8 | 9 | -- Register a @Component's constructor with the @Component's unique name. 10 | -- 11 | -- @name - The unique name for the @Component being registered. 12 | -- @constructor - The constructor method for the component. 13 | -- This method must be the same signature as @Component.new method. 14 | -- 15 | -- @return void 16 | function ComponentFactory.register(name, constructor) 17 | if factory[name] ~= nil then 18 | error(string.format("A constructor for %s has already been registered", name)) 19 | end 20 | factory[name] = constructor 21 | end 22 | 23 | function ComponentFactory.unregister(name) 24 | factory[name] = nil 25 | end 26 | 27 | function ComponentFactory.get_constructor(name) 28 | return factory[name] 29 | end 30 | 31 | return ComponentFactory 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Louis DeLosSantos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lua/ide/buffers/diffbuffer_test.lua: -------------------------------------------------------------------------------- 1 | local diff_buffer = require("ide.buffers.diffbuffer") 2 | 3 | local M = {} 4 | 5 | function M.test_write_diff() 6 | local diff_buf = diff_buffer.new() 7 | local lines_a = { "one", "two", "three" } 8 | local lines_b = { "one", "four", "three" } 9 | diff_buf.setup() 10 | diff_buf.write_lines(lines_a, "a") 11 | diff_buf.write_lines(lines_b, "b") 12 | diff_buf.diff() 13 | end 14 | 15 | function M.test_path_diff() 16 | vim.fn.writefile({ "one", "two", "three" }, "/tmp/a") 17 | vim.fn.writefile({ "one", "four", "three" }, "/tmp/b") 18 | 19 | local diff_buf = diff_buffer.new() 20 | diff_buf.setup() 21 | diff_buf.open_buffer("/tmp/a", "a") 22 | diff_buf.open_buffer("/tmp/b", "b") 23 | diff_buf.diff() 24 | 25 | vim.fn.delete("/tmp/a") 26 | vim.fn.delete("/tmp/b") 27 | end 28 | 29 | function M.test_path_mix() 30 | vim.fn.writefile({ "one", "two", "three" }, "/tmp/a") 31 | 32 | local diff_buf = diff_buffer.new() 33 | diff_buf.setup() 34 | diff_buf.open_buffer("/tmp/a", "a") 35 | 36 | local lines_b = { "one", "four", "three" } 37 | diff_buf.write_lines(lines_b, "b") 38 | diff_buf.diff() 39 | end 40 | 41 | return M 42 | -------------------------------------------------------------------------------- /doc/tags: -------------------------------------------------------------------------------- 1 | ide-bookmarks-components nvim-ide.txt /*ide-bookmarks-components* 2 | ide-branches-components nvim-ide.txt /*ide-branches-components* 3 | ide-bufferline-integration nvim-ide.txt /*ide-bufferline-integration* 4 | ide-bufferlist-components nvim-ide.txt /*ide-bufferlist-components* 5 | ide-callhierarchy-components nvim-ide.txt /*ide-callhierarchy-components* 6 | ide-changes-components nvim-ide.txt /*ide-changes-components* 7 | ide-commits-components nvim-ide.txt /*ide-commits-components* 8 | ide-components nvim-ide.txt /*ide-components* 9 | ide-contents nvim-ide.txt /*ide-contents* 10 | ide-default-components nvim-ide.txt /*ide-default-components* 11 | ide-explorer-components nvim-ide.txt /*ide-explorer-components* 12 | ide-intro nvim-ide.txt /*ide-intro* 13 | ide-nvim-dap-ui-integration nvim-ide.txt /*ide-nvim-dap-ui-integration* 14 | ide-outline-components nvim-ide.txt /*ide-outline-components* 15 | ide-panels nvim-ide.txt /*ide-panels* 16 | ide-plugin-integration nvim-ide.txt /*ide-plugin-integration* 17 | ide-terminalbrowser-components nvim-ide.txt /*ide-terminalbrowser-components* 18 | ide-timeline-components nvim-ide.txt /*ide-timeline-components* 19 | ide-usage nvim-ide.txt /*ide-usage* 20 | nvim-ide nvim-ide.txt /*nvim-ide* 21 | -------------------------------------------------------------------------------- /lua/ide/components/bufferlist/commands.lua: -------------------------------------------------------------------------------- 1 | local libcmd = require("ide.lib.commands") 2 | 3 | local Commands = {} 4 | 5 | Commands.new = function(buflist) 6 | assert(buflist ~= nil, "Cannot construct Commands without a BufferList instance") 7 | local self = { 8 | -- Instnace of BufferList component 9 | bufferlist = buflist, 10 | } 11 | 12 | -- returns a list of @Command(s) defined in 'ide.lib.commands' 13 | -- 14 | -- @return: @table, an array of @Command(s) which export an Explorer's 15 | -- command set. 16 | function self.get() 17 | return { 18 | libcmd.new( 19 | libcmd.KIND_ACTION, 20 | "BufferListFocus", 21 | "Focus", 22 | self.bufferlist.focus, 23 | { desc = "Open and focus the buffer list" } 24 | ), 25 | libcmd.new( 26 | libcmd.KIND_ACTION, 27 | "BufferListHide", 28 | "Hide", 29 | self.bufferlist.hide, 30 | { desc = "Hide the buffer list in its current panel. Use Focus to unhide." } 31 | ), 32 | libcmd.new( 33 | libcmd.KIND_ACTION, 34 | "BufferListEdit", 35 | "EditFile", 36 | self.bufferlist.open_buf, 37 | { desc = "Open the file for editing under the current cursor." } 38 | ), 39 | } 40 | end 41 | 42 | return self 43 | end 44 | 45 | return Commands 46 | -------------------------------------------------------------------------------- /lua/ide/workspaces/workspace_registry.lua: -------------------------------------------------------------------------------- 1 | -- WorkspaceRegistry is a registry associating Workspaces to tabs. 2 | -- 3 | -- It is a global singleton which other components may use after import. 4 | local WorkspaceRegistry = {} 5 | 6 | local registry = { 7 | ["1"] = nil, 8 | } 9 | 10 | function WorkspaceRegistry.register(workspace) 11 | if not vim.api.nvim_tabpage_is_valid(workspace.tab) then 12 | error(string.format("attempted to register workspace for non-existent tab %d", workspace.position)) 13 | end 14 | if registry[workspace.tab] == nil then 15 | registry[workspace.tab] = workspace 16 | return 17 | end 18 | if registry[workspace.tab] ~= nil then 19 | error( 20 | string.format( 21 | "attempted to registry workspace for tab %d but workspace already exist for tab", 22 | workspace.tab 23 | ) 24 | ) 25 | end 26 | registry[workspace.tab] = workspace 27 | end 28 | 29 | function WorkspaceRegistry.unregister(workspace) 30 | if registry[workspace.tab] == nil then 31 | return 32 | end 33 | -- close the workspace first 34 | registry[workspace.tab].close() 35 | -- remove from registry 36 | registry[workspace.tab] = nil 37 | end 38 | 39 | function WorkspaceRegistry.get_workspace(tab) 40 | return registry[tab] 41 | end 42 | 43 | return WorkspaceRegistry 44 | -------------------------------------------------------------------------------- /lua/ide/lib/encoding/base64.lua: -------------------------------------------------------------------------------- 1 | -- Original source and licence modified from: 2 | -- http://lua-users.org/wiki/BaseSixtyFour 3 | -- 4 | -- Lua 5.1+ base64 v3.0 (c) 2009 by Alex Kloss 5 | -- licensed under the terms of the LGPL2 6 | 7 | local Base64 = {} 8 | 9 | -- character table string 10 | local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 11 | 12 | -- encoding 13 | function Base64.encode(data) 14 | return ((data:gsub('.', function(x) 15 | local r,b='',x:byte() 16 | for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end 17 | return r; 18 | end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x) 19 | if (#x < 6) then return '' end 20 | local c=0 21 | for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end 22 | return b:sub(c+1,c+1) 23 | end)..({ '', '==', '=' })[#data%3+1]) 24 | end 25 | 26 | -- decoding 27 | function Base64.decode(data) 28 | data = string.gsub(data, '[^'..b..'=]', '') 29 | return (data:gsub('.', function(x) 30 | if (x == '=') then return '' end 31 | local r,f='',(b:find(x)-1) 32 | for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end 33 | return r; 34 | end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x) 35 | if (#x ~= 8) then return '' end 36 | local c=0 37 | for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end 38 | return string.char(c) 39 | end)) 40 | end 41 | 42 | return Base64 43 | -------------------------------------------------------------------------------- /lua/ide/components/branches/commands.lua: -------------------------------------------------------------------------------- 1 | local libcmd = require("ide.lib.commands") 2 | 3 | local Commands = {} 4 | 5 | Commands.new = function(branches) 6 | assert(branches ~= nil, "Cannot construct Commands without an branches instance.") 7 | local self = { 8 | -- An instance of an Explorer component which Commands delegates to. 9 | branches = branches, 10 | } 11 | 12 | -- returns a list of @Command(s) defined in 'ide.lib.commands' 13 | -- 14 | -- @return: @table, an array of @Command(s) which export an branches's 15 | -- command set. 16 | function self.get() 17 | local commands = { 18 | libcmd.new( 19 | libcmd.KIND_ACTION, 20 | "BranchesFocus", 21 | "Focus", 22 | self.branches.focus, 23 | { desc = "Open and focus the branches." } 24 | ), 25 | libcmd.new( 26 | libcmd.KIND_ACTION, 27 | "BranchesHide", 28 | "Hide", 29 | self.branches.hide, 30 | { desc = "Hide the branches in its current panel. Use Focus to unhide." } 31 | ), 32 | libcmd.new( 33 | libcmd.KIND_ACTION, 34 | "BranchesMinimize", 35 | "Minimize", 36 | self.branches.minimize, 37 | { desc = "Minimize the branches window in its panel." } 38 | ), 39 | libcmd.new( 40 | libcmd.KIND_ACTION, 41 | "BranchesMaximize", 42 | "Maximize", 43 | self.branches.maximize, 44 | { desc = "Maximize the branches window in its panel." } 45 | ), 46 | libcmd.new( 47 | libcmd.KIND_ACTION, 48 | "BranchesCreateBranch", 49 | "CreateBranch", 50 | self.branches.create_branch, 51 | { desc = "Create a branch" } 52 | ), 53 | } 54 | return commands 55 | end 56 | 57 | return self 58 | end 59 | 60 | return Commands 61 | -------------------------------------------------------------------------------- /lua/ide/trees/depth_table.lua: -------------------------------------------------------------------------------- 1 | local DepthTable = {} 2 | 3 | -- A DepthTable allows quick searching for nodes at a known tree depth. 4 | -- 5 | -- This can be useful when you need to find a Node at a known depth or you need 6 | -- to quickly get a list of all nodes at a given depth. 7 | DepthTable.new = function() 8 | local self = { 9 | table = { 10 | ['0'] = {}, 11 | }, 12 | } 13 | 14 | local function recursive_refresh(node) 15 | local depth = node.depth 16 | if self.table[depth] == nil then 17 | self.table[depth] = {} 18 | end 19 | 20 | table.insert(self.table[depth], node) 21 | 22 | -- recurse 23 | for _, child in ipairs(node.children) do 24 | recursive_refresh(child) 25 | end 26 | end 27 | 28 | function self.refresh(root) 29 | self.table = (function() return {} end)() 30 | recursive_refresh(root) 31 | end 32 | 33 | -- Search the DepthTable for a node with the @key at the given @depth 34 | -- 35 | -- @depth - integer, the depth at which to search for @key 36 | -- @key - the unique @Node.key to search for. 37 | -- return: @Node, @int - The node if found and the index within the array at 38 | -- the searched depth. 39 | function self.search(depth, key) 40 | local nodes = self.table[depth] 41 | if nodes == nil then 42 | return nil 43 | end 44 | for i, node in ipairs(nodes) do 45 | if node.key == key then 46 | return node, i 47 | end 48 | end 49 | return nil 50 | end 51 | 52 | return self 53 | end 54 | 55 | 56 | return DepthTable 57 | -------------------------------------------------------------------------------- /lua/ide/panels/panel_registry_test.lua: -------------------------------------------------------------------------------- 1 | local p_reg = require("ide.panels.panel_registry") 2 | local p = require("ide.panels.panel") 3 | 4 | local M = {} 5 | 6 | function M.test() 7 | local tp = p.new(1, p.PANEL_POS_TOP) 8 | local lp = p.new(1, p.PANEL_POS_LEFT) 9 | local rp = p.new(1, p.PANEL_POS_RIGHT) 10 | local bp = p.new(1, p.PANEL_POS_BOTTOM) 11 | 12 | p_reg.register(1, tp) 13 | p_reg.register(1, lp) 14 | p_reg.register(1, rp) 15 | p_reg.register(1, bp) 16 | 17 | if pcall(p_reg.register, 1, tp) then 18 | error("expected duplicate register of top panel to fail.") 19 | end 20 | 21 | local panels = p_reg.get_panels(1) 22 | 23 | if not vim.deep_equal(panels.top, tp) then 24 | error("retrieved top panel did not match constructed") 25 | end 26 | if not vim.deep_equal(panels.left, lp) then 27 | error("retrieved left panel did not match constructed") 28 | end 29 | if not vim.deep_equal(panels.right, rp) then 30 | error("retrieved right panel did not match constructed") 31 | end 32 | if not vim.deep_equal(panels.left, lp) then 33 | error("retrieved left panel did not match constructed") 34 | end 35 | 36 | p_reg.unregister(1, tp) 37 | p_reg.unregister(1, lp) 38 | p_reg.unregister(1, rp) 39 | p_reg.unregister(1, bp) 40 | 41 | panels = p_reg.get_panels(1) 42 | 43 | if panels.top ~= nil then 44 | error("expected top panel to be nil after unregistering") 45 | end 46 | if panels.left ~= nil then 47 | error("expected left panel to be nil after unregistering") 48 | end 49 | if panels.right ~= nil then 50 | error("expected right panel to be nil after unregistering") 51 | end 52 | if panels.bottom ~= nil then 53 | error("expected bottom panel to be nil after unregistering") 54 | end 55 | end 56 | 57 | return M 58 | -------------------------------------------------------------------------------- /lua/ide/lib/commands.lua: -------------------------------------------------------------------------------- 1 | local Command = {} 2 | 3 | local prototype = { 4 | name = nil, 5 | shortname = nil, 6 | callback = nil, 7 | kind = "action", 8 | opts = { 9 | desc = "", 10 | }, 11 | } 12 | 13 | -- Signifies the command performs an action when its callback field is invoked. 14 | Command.KIND_ACTION = "action" 15 | -- Signifies the command returns another list of @Command.prototype tables when 16 | -- its callback field is invoked. 17 | Command.KIND_SUBCOMMAND = "subcommand" 18 | 19 | -- A command is a description of a user command. 20 | -- 21 | -- Commands have a `kind` field informing callers how to handle the specific 22 | -- command. 23 | -- 24 | -- @kind - @string, one of the above KIND_* enums 25 | -- @name - @string, a unique name for the command outside the context of a 26 | -- submenu 27 | -- @shortname - @string, a non-unique name for the command in the context of a 28 | -- submenu 29 | -- @callback - a callback function to invoke the command. 30 | -- typically, if this is an "action" the function will receive the 31 | -- 'args' field explained in ":h nvim_create_user_command". 32 | -- if the type of "subcommand" the callback will return a list of 33 | -- @Command.prototype tables describing a another set of commands 34 | -- that exist under @Command.name. 35 | -- @opts - an options table as described in ":h nvim_create_user_command". 36 | Command.new = function(kind, name, shortname, callback, opts) 37 | local self = vim.deepcopy(prototype) 38 | self.kind = kind 39 | self.name = name 40 | self.shortname = shortname 41 | self.callback = callback 42 | self.opts = opts 43 | 44 | return self 45 | end 46 | 47 | return Command 48 | -------------------------------------------------------------------------------- /lua/ide/lib/async_client_test.lua: -------------------------------------------------------------------------------- 1 | local client = require("ide.lib.async_client") 2 | 3 | local uv = vim.loop 4 | 5 | local M = {} 6 | 7 | function M.test_stdout() 8 | -- use an echo client for testing. 9 | local c = client.new("echo") 10 | c.make_request("-n hello world", nil, function(result) 11 | print(vim.inspect(result)) 12 | assert(result.error == false) 13 | assert(result.stderr == "") 14 | assert(result.stdout == "hello world") 15 | end) 16 | c.make_request(nil, nil, function(result) 17 | print(vim.inspect(result)) 18 | assert(result.error == false) 19 | assert(result.stderr == "") 20 | assert(result.stdout == "\n") 21 | end) 22 | c.make_request("-n hello world", nil, function(result) 23 | print(vim.inspect(result)) 24 | assert(result.error == false) 25 | assert(result.stderr == "") 26 | assert(#result.stdout == 2) 27 | assert(result.stdout[1] == "hello") 28 | assert(result.stdout[2] == "world") 29 | end, function(req) 30 | req.stdout = vim.fn.split(req.stdout) 31 | end) 32 | c.make_nl_request([[-n hello\nworld]], nil, function(result) 33 | print(vim.inspect(result)) 34 | assert(result.error == false) 35 | assert(result.stderr == "") 36 | assert(#result.stdout == 2) 37 | assert(result.stdout[1] == "hello") 38 | assert(result.stdout[2] == "world") 39 | end) 40 | c.make_json_request([[-n {"hello": "world"}]], nil, function(result) 41 | print(vim.inspect(result)) 42 | assert(result.error == false) 43 | assert(result.stderr == "") 44 | assert(result.stdout["hello"] == "world") 45 | end) 46 | -- test json_code fail 47 | c.make_json_request([[-n {"hello", "world"}]], nil, function(result) 48 | print(vim.inspect(result)) 49 | assert(result.error == true) 50 | assert(result.stderr == "") 51 | end) 52 | end 53 | 54 | return M 55 | -------------------------------------------------------------------------------- /lua/ide/icons/nerd_icon_set.lua: -------------------------------------------------------------------------------- 1 | local icon_set = require("ide.icons.icon_set") 2 | 3 | local NerdIconSet = {} 4 | 5 | local prototype = { 6 | icons = { 7 | Account = "", 8 | Array = "", 9 | Bookmark = "", 10 | Boolean = "", 11 | Calendar = "", 12 | Check = "", 13 | CheckAll = "", 14 | Circle = "", 15 | CircleFilled = "", 16 | CirclePause = "", 17 | CircleSlash = "", 18 | CircleStop = "", 19 | Class = "ﴯ", 20 | Code = "", 21 | Collapsed = "", 22 | Color = "", 23 | Comment = "", 24 | Constant = "", 25 | Constructor = "", 26 | DiffAdded = "", 27 | Enum = "", 28 | EnumMember = "", 29 | Event = "", 30 | Expanded = "", 31 | Field = "ﰠ", 32 | File = "", 33 | Folder = "", 34 | Function = "", 35 | GitBranch = "", 36 | GitCommit = "ﰖ", 37 | GitCompare = "", 38 | GitIssue = "", 39 | GitMerge = "שּׁ", 40 | GitPullRequest = "", 41 | GitRepo = "", 42 | IndentGuide = "│", 43 | IndentGuideEnd = "┕", 44 | Info = "", 45 | Interface = "", 46 | Key = "", 47 | Keyword = "", 48 | Method = "", 49 | Module = "", 50 | MultiComment = "", 51 | Namespace = "", 52 | Notebook = "ﴬ", 53 | Null = "ﳠ", 54 | Number = "", 55 | Object = "", 56 | Operator = "", 57 | Package = "", 58 | Pass = "", 59 | PassFilled = "", 60 | Pencil = "", 61 | Property = "ﰠ", 62 | Reference = "", 63 | Separator = "•", 64 | Snippet = "", 65 | Space = " ", 66 | String = "", 67 | Struct = "פּ", 68 | Text = "", 69 | Terminal = "", 70 | TypeParameter = "", 71 | Unit = "塞", 72 | Value = "", 73 | Variable = "", 74 | }, 75 | } 76 | 77 | NerdIconSet.new = function() 78 | local self = icon_set.new() 79 | self.icons = prototype.icons 80 | return self 81 | end 82 | 83 | return NerdIconSet 84 | -------------------------------------------------------------------------------- /lua/ide/components/changes/statusnode.lua: -------------------------------------------------------------------------------- 1 | local node = require("ide.trees.node") 2 | local icons = require("ide.icons") 3 | local libpopup = require("ide.lib.popup") 4 | local libwin = require("ide.lib.win") 5 | 6 | local StatusNode = {} 7 | 8 | StatusNode.new = function(status, path, staged, depth) 9 | local key = string.format("%s:%s", path, vim.inspect(staged)) 10 | local self = node.new("status", status, key, depth) 11 | self.status = status 12 | self.path = path 13 | self.staged = staged 14 | 15 | -- Marshal a statusnode into a buffer line. 16 | -- 17 | -- @return: @icon - @string, icon for status's kind 18 | -- @name - @string, status's name 19 | -- @details - @string, status's detail if exists. 20 | function self.marshal() 21 | local icon = "" 22 | -- use webdev icons if possible 23 | if pcall(require, "nvim-web-devicons") then 24 | icon = require("nvim-web-devicons").get_icon(self.path, nil, { default = true }) 25 | end 26 | if self.depth == 1 then 27 | icon = icons.global_icon_set.get_icon("GitCompare") 28 | end 29 | if vim.fn.isdirectory(self.path) ~= 0 then 30 | icon = icons.global_icon_set.get_icon("Folder") 31 | end 32 | if self.depth == 0 then 33 | icon = icons.global_icon_set.get_icon("GitRepo") 34 | end 35 | local name = self.path 36 | local detail = self.status 37 | return icon, name, detail 38 | end 39 | 40 | local popup_win = nil 41 | function self.details() 42 | if libwin.win_is_valid(popup_win) then 43 | vim.api.nvim_set_current_win(popup_win) 44 | return 45 | end 46 | 47 | local lines = {} 48 | table.insert(lines, string.format("%s %s %s", icons.global_icon_set.get_icon("File"), self.path, self.status)) 49 | 50 | popup_win = libpopup.until_cursor_move(lines) 51 | end 52 | 53 | return self 54 | end 55 | 56 | return StatusNode 57 | -------------------------------------------------------------------------------- /lua/ide/components/changes/commands.lua: -------------------------------------------------------------------------------- 1 | local libcmd = require('ide.lib.commands') 2 | 3 | local Commands = {} 4 | 5 | Commands.new = function(changes) 6 | assert(changes ~= nil, "Cannot construct Commands without an Changes instance.") 7 | local self = { 8 | -- An instance of an Explorer component which Commands delegates to. 9 | changes = changes, 10 | } 11 | 12 | -- returns a list of @Command(s) defined in 'ide.lib.commands' 13 | -- 14 | -- @return: @table, an array of @Command(s) which export an Changes's 15 | -- command set. 16 | function self.get() 17 | local commands = { 18 | libcmd.new( 19 | libcmd.KIND_ACTION, 20 | "ChangesFocus", 21 | "Focus", 22 | self.changes.focus, 23 | { desc = "Open and focus the Changes." } 24 | ), 25 | libcmd.new( 26 | libcmd.KIND_ACTION, 27 | "ChangesHide", 28 | "Hide", 29 | self.changes.hide, 30 | { desc = "Hide the changes in its current panel. Use Focus to unhide." } 31 | ), 32 | libcmd.new( 33 | libcmd.KIND_ACTION, 34 | "ChangesMinimize", 35 | "Minimize", 36 | self.changes.minimize, 37 | { desc = "Minimize the changes window in its panel." } 38 | ), 39 | libcmd.new( 40 | libcmd.KIND_ACTION, 41 | "ChangesMaximize", 42 | "Maximize", 43 | self.changes.maximize, 44 | { desc = "Maximize the changes window in its panel." } 45 | ), 46 | } 47 | return commands 48 | end 49 | 50 | return self 51 | end 52 | 53 | return Commands 54 | -------------------------------------------------------------------------------- /lua/ide/components/commits/commands.lua: -------------------------------------------------------------------------------- 1 | local libcmd = require('ide.lib.commands') 2 | 3 | local Commands = {} 4 | 5 | Commands.new = function(commits) 6 | assert(commits ~= nil, "Cannot construct Commands without an Commits instance.") 7 | local self = { 8 | -- An instance of an Explorer component which Commands delegates to. 9 | commits = commits, 10 | } 11 | 12 | -- returns a list of @Command(s) defined in 'ide.lib.commands' 13 | -- 14 | -- @return: @table, an array of @Command(s) which export an Commits's 15 | -- command set. 16 | function self.get() 17 | local commands = { 18 | libcmd.new( 19 | libcmd.KIND_ACTION, 20 | "CommitsFocus", 21 | "Focus", 22 | self.commits.focus, 23 | { desc = "Open and focus the Commits." } 24 | ), 25 | libcmd.new( 26 | libcmd.KIND_ACTION, 27 | "CommitsHide", 28 | "Hide", 29 | self.commits.hide, 30 | { desc = "Hide the commits in its current panel. Use Focus to unhide." } 31 | ), 32 | libcmd.new( 33 | libcmd.KIND_ACTION, 34 | "CommitsMinimize", 35 | "Minimize", 36 | self.commits.minimize, 37 | { desc = "Minimize the commits window in its panel." } 38 | ), 39 | libcmd.new( 40 | libcmd.KIND_ACTION, 41 | "CommitsMaximize", 42 | "Maximize", 43 | self.commits.maximize, 44 | { desc = "Maximize the commits window in its panel." } 45 | ), 46 | } 47 | return commands 48 | end 49 | 50 | return self 51 | end 52 | 53 | return Commands 54 | -------------------------------------------------------------------------------- /lua/ide/components/terminal/commands.lua: -------------------------------------------------------------------------------- 1 | local libcmd = require('ide.lib.commands') 2 | 3 | local Commands = {} 4 | 5 | Commands.new = function(terminal) 6 | assert(terminal ~= nil, "Cannot construct Commands without an Terminal instance.") 7 | local self = { 8 | -- An instance of an Explorer component which Commands delegates to. 9 | terminal = terminal, 10 | } 11 | 12 | -- returns a list of @Command(s) defined in 'ide.lib.commands' 13 | -- 14 | -- @return: @table, an array of @Command(s) which export an Terminal's 15 | -- command set. 16 | function self.get() 17 | local commands = { 18 | libcmd.new( 19 | libcmd.KIND_ACTION, 20 | "TerminalFocus", 21 | "Focus", 22 | self.terminal.focus, 23 | { desc = "Open and focus the Terminal." } 24 | ), 25 | libcmd.new( 26 | libcmd.KIND_ACTION, 27 | "TerminalHide", 28 | "Hide", 29 | self.terminal.hide, 30 | { desc = "Hide the terminal in its current panel. Use Focus to unhide." } 31 | ), 32 | libcmd.new( 33 | libcmd.KIND_ACTION, 34 | "TerminalMinimize", 35 | "Minimize", 36 | self.terminal.minimize, 37 | { desc = "Minimize the terminal window in its panel." } 38 | ), 39 | libcmd.new( 40 | libcmd.KIND_ACTION, 41 | "TerminalMaximize", 42 | "Maximize", 43 | self.terminal.maximize, 44 | { desc = "Maximize the terminal window in its panel." } 45 | ), 46 | } 47 | return commands 48 | end 49 | 50 | return self 51 | end 52 | 53 | return Commands 54 | -------------------------------------------------------------------------------- /lua/ide/panels/panel_registry.lua: -------------------------------------------------------------------------------- 1 | -- PanelRegistry is a registry associating panels to tabs. 2 | -- 3 | -- It is a global singleton which other components may use after import. 4 | local PanelRegistry = {} 5 | 6 | local registry = { 7 | ["1"] = { 8 | top = nil, 9 | bottom = nil, 10 | left = nil, 11 | right = nil, 12 | }, 13 | } 14 | 15 | local prototype = { 16 | top = nil, 17 | bottom = nil, 18 | left = nil, 19 | right = nil, 20 | } 21 | 22 | function PanelRegistry.register(panel) 23 | if not vim.api.nvim_tabpage_is_valid(panel.tab) then 24 | error(string.format("attempted to register %s panel for non-existent tab %d", panel.tab, panel.position)) 25 | end 26 | if registry[panel.tab] == nil then 27 | registry[panel.tab] = vim.deepcopy(prototype) 28 | registry[panel.tab][panel.position] = panel 29 | return 30 | end 31 | if registry[panel.tab][panel.position] ~= nil then 32 | error( 33 | string.format( 34 | "attempted to registry %s panel for tab %d but panel already exist for position.", 35 | panel.position, 36 | panel.tab 37 | ) 38 | ) 39 | end 40 | registry[panel.tab][panel.position] = panel 41 | end 42 | 43 | function PanelRegistry.unregister(panel) 44 | if panel == nil then 45 | return 46 | end 47 | if registry[panel.tab] == nil then 48 | return 49 | end 50 | if registry[panel.tab].top ~= nil then 51 | registry[panel.tab].top.close() 52 | end 53 | if registry[panel.tab].left ~= nil then 54 | registry[panel.tab].left.close() 55 | end 56 | if registry[panel.tab].right ~= nil then 57 | registry[panel.tab].right.close() 58 | end 59 | if registry[panel.tab].bottom ~= nil then 60 | registry[panel.tab].bottom.close() 61 | end 62 | registry[panel.tab] = vim.deepcopy(prototype) 63 | end 64 | 65 | function PanelRegistry.get_panels(tab) 66 | if tab == nil then 67 | return registry 68 | end 69 | return registry[tab] 70 | end 71 | 72 | return PanelRegistry 73 | -------------------------------------------------------------------------------- /lua/ide/icons/codicon_icon_set.lua: -------------------------------------------------------------------------------- 1 | local icon_set = require("ide.icons.icon_set") 2 | 3 | local CodiconIconSet = {} 4 | 5 | local prototype = { 6 | icons = { 7 | Account = "", 8 | Array = "", 9 | Bookmark = "", 10 | Boolean = "", 11 | Calendar = "", 12 | Check = "", 13 | CheckAll = "", 14 | Circle = "", 15 | CircleFilled = "", 16 | CirclePause = "", 17 | CircleSlash = "", 18 | CircleStop = "", 19 | Class = "", 20 | Collapsed = "", 21 | Color = "", 22 | Code = "", 23 | Comment = "", 24 | CommentExclaim = "", 25 | Constant = "", 26 | Constructor = "", 27 | DiffAdded = "", 28 | Enum = "", 29 | EnumMember = "", 30 | Event = "", 31 | Expanded = "", 32 | Field = "", 33 | File = "", 34 | Folder = "", 35 | Function = "", 36 | GitBranch = "", 37 | GitCommit = "", 38 | GitCompare = "", 39 | GitIssue = "", 40 | GitMerge = "", 41 | GitPullRequest = "", 42 | GitRepo = "", 43 | History = "", 44 | IndentGuide = "│", 45 | IndentGuideEnd = "┕", 46 | Info = "", 47 | Interface = "", 48 | Key = "", 49 | Keyword = "", 50 | Method = "", 51 | Module = "", 52 | MultiComment = "", 53 | Namespace = "", 54 | Notebook = "", 55 | Notification = "", 56 | Null = "", 57 | Number = "", 58 | Object = "", 59 | Operator = "", 60 | Package = "", 61 | Pass = "", 62 | PassFilled = "", 63 | Pencil = "", 64 | Property = "", 65 | Reference = "", 66 | RequestChanges = "", 67 | Separator = "•", 68 | Snippet = "", 69 | Space = " ", 70 | String = "", 71 | Struct = "", 72 | Sync = "", 73 | Text = "", 74 | Terminal = "", 75 | TypeParameter = "", 76 | Unit = "", 77 | Value = "", 78 | Variable = "", 79 | }, 80 | } 81 | 82 | CodiconIconSet.new = function() 83 | local self = icon_set.new() 84 | self.icons = prototype.icons 85 | return self 86 | end 87 | 88 | return CodiconIconSet 89 | -------------------------------------------------------------------------------- /lua/ide/workspaces/workspace_test.lua: -------------------------------------------------------------------------------- 1 | local workspace = require('ide.workspaces.workspace') 2 | local panel = require('ide.panels.panel') 3 | local component_factory = require('ide.panels.component_factory') 4 | local test_component = require('ide.panels.test_component') 5 | 6 | local M = {} 7 | 8 | function M.test_default() 9 | -- register a component with the component factory 10 | component_factory.register("test-component", test_component.new) 11 | 12 | -- create a workspace for the current tab. 13 | local ws = workspace.new(vim.api.nvim_get_current_tabpage()) 14 | 15 | -- open the workspace 16 | ws.init() 17 | ws.panels[panel.PANEL_POS_TOP].open() 18 | ws.panels[panel.PANEL_POS_LEFT].open() 19 | ws.panels[panel.PANEL_POS_RIGHT].open() 20 | ws.panels[panel.PANEL_POS_BOTTOM].open() 21 | -- ws.close() 22 | end 23 | 24 | function M.test_custom() 25 | local custom_config = { 26 | -- A unique name for this workspace 27 | name = nil, 28 | -- Defines which panels will be displayed in this workspace along with 29 | -- a list of component names to register to the displayed panel. 30 | -- 31 | -- Each key associates a list of component names that should we registered 32 | -- for that panel. 33 | -- 34 | -- If the associated list is empyt for a panel at a given position it is 35 | -- assumed a panel at that position will not be used and the @Workspace will 36 | -- not instantiate a panel there. 37 | panels = { 38 | top = {}, 39 | left = { "test-component", "test-component" }, 40 | right = { "test-component" }, 41 | bottom = { "test-component" } 42 | } 43 | } 44 | -- register a component with the component factory 45 | component_factory.register("test-component", test_component.new) 46 | 47 | -- create a workspace for the current tab. 48 | local ws = workspace.new(vim.api.nvim_get_current_tabpage(), custom_config) 49 | 50 | -- open the workspace 51 | ws.init() 52 | ws.panels[panel.PANEL_POS_LEFT].open() 53 | ws.panels[panel.PANEL_POS_RIGHT].open() 54 | ws.panels[panel.PANEL_POS_BOTTOM].open() 55 | -- ws.close() 56 | end 57 | 58 | return M 59 | -------------------------------------------------------------------------------- /lua/ide/buffers/doomscrollbuffer.lua: -------------------------------------------------------------------------------- 1 | local buffer = require("ide.buffers.buffer") 2 | local libwin = require("ide.lib.win") 3 | local autocmd = require("ide.lib.autocmd") 4 | 5 | local DoomScrollBuffer = {} 6 | 7 | -- A DoomScrollBuffer is a buffer which calls the on_scroll callback 8 | -- each time it detects the cursor has moved to the final line of 9 | -- the buffer. 10 | -- 11 | -- @on_scroll - @function(@DoomScrollBuffer), a callback functions which 12 | -- must return an array of @string, each of which is a line to 13 | -- append to the buffer. 14 | -- the function is called with the @DoomScrollBuffer performing the 15 | -- callback. 16 | DoomScrollBuffer.new = function(on_scroll, buf, listed, scratch, opts) 17 | local self = buffer.new(buf, listed, scratch) 18 | self.on_scroll = on_scroll 19 | self.on_scroll_aucmd = nil 20 | self.debouncing = false 21 | 22 | function self.doomscroll_aucmd(args) 23 | local event_win = vim.api.nvim_get_current_win() 24 | if not libwin.win_is_valid(event_win) then 25 | return 26 | end 27 | local buf = vim.api.nvim_win_get_buf(event_win) 28 | if buf == self.buf then 29 | local cursor = libwin.get_cursor(event_win) 30 | if cursor == nil then 31 | return 32 | end 33 | if cursor[1] == vim.api.nvim_buf_line_count(self.buf) then 34 | local lines = on_scroll(self) 35 | if lines ~= nil then 36 | self.write_lines(lines) 37 | end 38 | end 39 | end 40 | end 41 | 42 | autocmd.buf_enter_and_leave(self.buf, function() 43 | self.on_scroll_aucmd = vim.api.nvim_create_autocmd({ "CursorHold" }, { 44 | callback = function() 45 | if not self.debouncing then 46 | self.doomscroll_aucmd() 47 | self.debouncing = true 48 | vim.defer_fn(function() 49 | self.debouncing = false 50 | end, 350) 51 | end 52 | end, 53 | }) 54 | end, function() 55 | if self.on_scroll_aucmd ~= nil then 56 | vim.api.nvim_del_autocmd(self.on_scroll_aucmd) 57 | self.on_scroll_aucmd = nil 58 | end 59 | end) 60 | 61 | return self 62 | end 63 | 64 | return DoomScrollBuffer 65 | -------------------------------------------------------------------------------- /lua/ide/lib/popup.lua: -------------------------------------------------------------------------------- 1 | local buffer = require("ide.buffers.buffer") 2 | local icons = require("ide.icons") 3 | local libwin = require('ide.lib.win') 4 | 5 | local Popup = {} 6 | 7 | function calculate_dimensions(lines) 8 | local h = #lines 9 | if h > 100 then 10 | h = 100 11 | end 12 | local w = 0 13 | for _, l in ipairs(lines) do 14 | if #l > w then 15 | w = #l 16 | end 17 | end 18 | if w > 100 then 19 | w = 100 20 | end 21 | return h, w 22 | end 23 | 24 | function Popup.enter_until_exit(lines) 25 | local buf = buffer.new(nil, false, true) 26 | buf.write_lines(lines) 27 | 28 | local h, w = calculate_dimensions(lines) 29 | 30 | local win = vim.api.nvim_open_win(buf.buf, true, { 31 | relative = "cursor", 32 | col = 0, 33 | row = 0, 34 | width = w, 35 | height = h, 36 | zindex = 99, 37 | style = "minimal", 38 | border = "rounded", 39 | noautocmd = true, 40 | }) 41 | 42 | -- set any icon highlights 43 | icons.global_icon_set.set_win_highlights() 44 | local aucmd = nil 45 | aucmd = vim.api.nvim_create_autocmd({ "WinLeave" }, { 46 | callback = function() 47 | vim.api.nvim_win_close(win, true) 48 | vim.api.nvim_del_autocmd(aucmd) 49 | end, 50 | }) 51 | end 52 | 53 | function Popup.until_cursor_move(lines) 54 | local buf = buffer.new(nil, false, true) 55 | buf.write_lines(lines) 56 | 57 | local h, w = calculate_dimensions(lines) 58 | 59 | local win = vim.api.nvim_open_win(buf.buf, false, { 60 | relative = "cursor", 61 | col = 0, 62 | row = 0, 63 | width = w, 64 | height = h, 65 | zindex = 99, 66 | style = "minimal", 67 | border = "rounded", 68 | noautocmd = true, 69 | }) 70 | -- set any icon highlights 71 | icons.global_icon_set.set_win_highlights() 72 | local aucmd = nil 73 | aucmd = vim.api.nvim_create_autocmd({ "CursorMoved" }, { 74 | callback = function() 75 | -- if we are inside the popup window just return 76 | local cur_win = vim.api.nvim_get_current_win() 77 | if cur_win == win then 78 | return 79 | end 80 | 81 | if libwin.win_is_valid(win) then 82 | vim.api.nvim_win_close(win, true) 83 | end 84 | vim.api.nvim_del_autocmd(aucmd) 85 | end, 86 | }) 87 | 88 | return win 89 | end 90 | 91 | return Popup 92 | -------------------------------------------------------------------------------- /lua/ide/lib/win.lua: -------------------------------------------------------------------------------- 1 | local Win = {} 2 | 3 | -- Reports true if the window is both non-nil and valid. 4 | function Win.win_is_valid(win) 5 | if win ~= nil and vim.api.nvim_win_is_valid(win) then 6 | return true 7 | end 8 | return false 9 | end 10 | 11 | function Win.safe_cursor_restore(win, cursor) 12 | if not Win.win_is_valid(win) then 13 | return false 14 | end 15 | local buf = vim.api.nvim_win_get_buf(win) 16 | local lc = vim.api.nvim_buf_line_count(buf) 17 | if cursor[1] > lc then 18 | cursor[1] = lc 19 | end 20 | vim.api.nvim_win_set_cursor(win, cursor) 21 | end 22 | 23 | function Win.get_cursor(win) 24 | if not Win.win_is_valid(win) then 25 | return 26 | end 27 | return vim.api.nvim_win_get_cursor(win) 28 | end 29 | 30 | -- Returns the current cursor for `win` and also a function which can be called 31 | -- to restore the cursor to this position. 32 | function Win.get_cursor_with_restore(win) 33 | if not Win.win_is_valid(win) then 34 | return 35 | end 36 | local cursor = vim.api.nvim_win_get_cursor(win) 37 | return cursor, function() 38 | if Win.win_is_valid(win) then 39 | Win.safe_cursor_restore(win, cursor) 40 | end 41 | end 42 | end 43 | 44 | function Win.restore_cur_win() 45 | local cur_win = vim.api.nvim_get_current_win() 46 | return function() 47 | if Win.win_is_valid(cur_win) then 48 | vim.api.nvim_set_current_win(cur_win) 49 | end 50 | end 51 | end 52 | 53 | function Win.is_component_win(win) 54 | local buf = vim.api.nvim_win_get_buf(win) 55 | local name = vim.api.nvim_buf_get_name(buf) 56 | if vim.fn.match(name, "component://") >= 0 then 57 | return true 58 | end 59 | return false 60 | end 61 | 62 | function Win.get_buf(win) 63 | return vim.api.nvim_win_get_buf(win) 64 | end 65 | 66 | function Win.set_winbar_title(win, str) 67 | if Win.win_is_valid(win) then 68 | vim.api.nvim_win_set_option(win, "winbar", vim.fn.toupper(str)) 69 | end 70 | end 71 | 72 | function Win.open_buffer(win, path) 73 | if not Win.win_is_valid(win) then 74 | return 75 | end 76 | vim.api.nvim_set_current_win(win) 77 | vim.cmd("edit " .. path) 78 | end 79 | 80 | function Win.set_option_with_restore(win, option, value) 81 | local cur = vim.api.nvim_win_get_option(win, option) 82 | vim.api.nvim_win_set_option(win, option, value) 83 | return function() 84 | if Win.win_is_valid(win) then 85 | vim.api.nvim_win_set_option(win, option, cur) 86 | end 87 | end 88 | end 89 | 90 | return Win 91 | -------------------------------------------------------------------------------- /lua/ide/components/timeline/timelinenode.lua: -------------------------------------------------------------------------------- 1 | local node = require("ide.trees.node") 2 | local icons = require("ide.icons") 3 | local git = require("ide.lib.git.client").new() 4 | local libpopup = require("ide.lib.popup") 5 | local libwin = require("ide.lib.win") 6 | 7 | local TimelineNode = {} 8 | 9 | TimelineNode.new = function(sha, file, subject, author, date, depth) 10 | -- extends 'ide.trees.Node' fields. 11 | 12 | local self = node.new("git_commit", sha, sha, depth) 13 | 14 | -- TimelineNodes make a list, not a tree, so just always expand and we'll set 15 | -- the tree to marshal with no leave guides. 16 | self.expanded = true 17 | self.sha = sha 18 | self.file = file 19 | self.subject = subject 20 | self.author = author 21 | self.date = date 22 | 23 | -- Marshal a timelinenode into a buffer line. 24 | -- 25 | -- @return: @icon - @string, icon used for call hierarchy item 26 | -- @name - @string, the name of the call hierarchy item 27 | -- @details - @string, the details of the call hierarchy item 28 | function self.marshal() 29 | local icon = icons.global_icon_set.get_icon("GitCommit") 30 | -- root is the file we are displaying the timeline for. 31 | if self.depth == 0 then 32 | icon = icons.global_icon_set.get_icon("File") 33 | end 34 | local name = string.format("%s", self.subject) 35 | local detail = string.format("%s %s", self.author, self.date) 36 | return icon, name, detail 37 | end 38 | 39 | local popup_win = nil 40 | function self.details() 41 | if libwin.win_is_valid(popup_win) then 42 | vim.api.nvim_set_current_win(popup_win) 43 | return 44 | end 45 | 46 | git.log(self.sha, 1, function(data) 47 | if data == nil then 48 | return 49 | end 50 | 51 | local commit = data[1] 52 | if commit == nil then 53 | return 54 | end 55 | 56 | local lines = {} 57 | table.insert(lines, string.format("%s %s", icons.global_icon_set.get_icon("GitCommit"), commit.sha)) 58 | table.insert(lines, string.format("%s %s", icons.global_icon_set.get_icon("Account"), commit.author)) 59 | table.insert(lines, string.format("%s %s", icons.global_icon_set.get_icon("Calendar"), commit.date)) 60 | table.insert(lines, "") 61 | 62 | local subject = vim.fn.split(commit.subject, "\n") 63 | table.insert(lines, subject[1]) 64 | 65 | table.insert(lines, "") 66 | 67 | local body = vim.fn.split(commit.body, "\n") 68 | for _, l in ipairs(body) do 69 | table.insert(lines, l) 70 | end 71 | 72 | popup_win = libpopup.until_cursor_move(lines) 73 | end) 74 | end 75 | 76 | return self 77 | end 78 | 79 | return TimelineNode 80 | -------------------------------------------------------------------------------- /lua/ide/components/bookmarks/commands.lua: -------------------------------------------------------------------------------- 1 | local libcmd = require("ide.lib.commands") 2 | 3 | local Commands = {} 4 | 5 | Commands.new = function(bookmarks) 6 | assert(bookmarks ~= nil, "Cannot construct Commands without an Bookmarks instance.") 7 | local self = { 8 | -- An instance of an Explorer component which Commands delegates to. 9 | bookmarks = bookmarks, 10 | } 11 | 12 | -- returns a list of @Command(s) defined in 'ide.lib.commands' 13 | -- 14 | -- @return: @table, an array of @Command(s) which export an Bookmarks's 15 | -- command set. 16 | function self.get() 17 | local commands = { 18 | libcmd.new( 19 | libcmd.KIND_ACTION, 20 | "BookmarksFocus", 21 | "Focus", 22 | self.bookmarks.focus, 23 | { desc = "Open and focus the Bookmarks." } 24 | ), 25 | libcmd.new( 26 | libcmd.KIND_ACTION, 27 | "BookmarksHide", 28 | "Hide", 29 | self.bookmarks.hide, 30 | { desc = "Hide the bookmarks in its current panel. Use Focus to unhide." } 31 | ), 32 | libcmd.new( 33 | libcmd.KIND_ACTION, 34 | "BookmarksMinimize", 35 | "Minimize", 36 | self.bookmarks.minimize, 37 | { desc = "Minimize the bookmarks window in its panel." } 38 | ), 39 | libcmd.new( 40 | libcmd.KIND_ACTION, 41 | "BookmarksMaximize", 42 | "Maximize", 43 | self.bookmarks.maximize, 44 | { desc = "Maximize the bookmarks window in its panel." } 45 | ), 46 | libcmd.new( 47 | libcmd.KIND_ACTION, 48 | "BookmarksCreateNotebook", 49 | "CreateNotebook", 50 | self.bookmarks.create_notebook, 51 | { desc = "Create a new Notebook for the current project." } 52 | ), 53 | libcmd.new( 54 | libcmd.KIND_ACTION, 55 | "BookmarksOpenNotebook", 56 | "OpenNotebook", 57 | self.bookmarks.open_notebook, 58 | { desc = "Create a new Notebook for the current project." } 59 | ), 60 | libcmd.new( 61 | libcmd.KIND_ACTION, 62 | "BookmarksRemoveNotebook", 63 | "RemoveNotebook", 64 | self.bookmarks.remove_notebook, 65 | { desc = "Remove a Notebook for the current project." } 66 | ), 67 | libcmd.new( 68 | libcmd.KIND_ACTION, 69 | "BookmarksCreate", 70 | "CreateBookmark", 71 | self.bookmarks.create_bookmark, 72 | { desc = "Create a bookmark within the opened Notebook." } 73 | ), 74 | libcmd.new( 75 | libcmd.KIND_ACTION, 76 | "BookmarksRemove", 77 | "RemoveBookmark", 78 | self.bookmarks.remove_bookmark, 79 | { desc = "Create a bookmark within the opened Notebook." } 80 | ), 81 | } 82 | return commands 83 | end 84 | 85 | return self 86 | end 87 | 88 | return Commands 89 | -------------------------------------------------------------------------------- /lua/ide/lib/sort.lua: -------------------------------------------------------------------------------- 1 | -- Modified from the MIT-licensed code 2 | -- at https://gist.github.com/1bardesign/62b90260e47ea807864fc3cc8f880f8d 3 | 4 | --tunable size for 5 | local max_chunk_size = 24 6 | 7 | local function insertion_sort_impl(array, first, last, less) 8 | for i = first + 1, last do 9 | local k = first 10 | local v = array[i] 11 | for j = i, first + 1, -1 do 12 | if less(v, array[j - 1]) then 13 | array[j] = array[j - 1] 14 | else 15 | k = j 16 | break 17 | end 18 | end 19 | array[k] = v 20 | end 21 | end 22 | 23 | local function merge(array, workspace, low, middle, high, less) 24 | local i, j, k 25 | i = 1 26 | -- copy first half of array to auxiliary array 27 | for j = low, middle do 28 | workspace[i] = array[j] 29 | i = i + 1 30 | end 31 | -- sieve through 32 | i = 1 33 | j = middle + 1 34 | k = low 35 | while true do 36 | if (k >= j) or (j > high) then 37 | break 38 | end 39 | if less(array[j], workspace[i]) then 40 | array[k] = array[j] 41 | j = j + 1 42 | else 43 | array[k] = workspace[i] 44 | i = i + 1 45 | end 46 | k = k + 1 47 | end 48 | -- copy back any remaining elements of first half 49 | for k = k, j - 1 do 50 | array[k] = workspace[i] 51 | i = i + 1 52 | end 53 | end 54 | 55 | local function merge_sort_impl(array, workspace, low, high, less) 56 | if high - low <= max_chunk_size then 57 | insertion_sort_impl(array, low, high, less) 58 | else 59 | local middle = math.floor((low + high) / 2) 60 | merge_sort_impl(array, workspace, low, middle, less) 61 | merge_sort_impl(array, workspace, middle + 1, high, less) 62 | merge(array, workspace, low, middle, high, less) 63 | end 64 | end 65 | 66 | --inline common setup stuff 67 | local function sort_setup(array, less) 68 | local n = #array 69 | local trivial = false 70 | --trivial cases; empty or 1 element 71 | if n <= 1 then 72 | trivial = true 73 | else 74 | --default less 75 | less = less or function(a, b) 76 | return a < b 77 | end 78 | --check less 79 | if less(array[1], array[1]) then 80 | error("invalid order function for sorting") 81 | end 82 | end 83 | --setup complete 84 | return trivial, n, less 85 | end 86 | 87 | local function mergesort(array, less) 88 | --setup 89 | local trivial, n, less = sort_setup(array, less) 90 | if not trivial then 91 | --temp storage 92 | local workspace = {} 93 | workspace[math.floor((n + 1) / 2)] = array[1] 94 | --dive in 95 | merge_sort_impl(array, workspace, 1, n, less) 96 | end 97 | return array 98 | end 99 | 100 | return mergesort 101 | -------------------------------------------------------------------------------- /lua/ide/components/outline/commands.lua: -------------------------------------------------------------------------------- 1 | local libcmd = require('ide.lib.commands') 2 | 3 | local Commands = {} 4 | 5 | Commands.new = function(outline) 6 | assert(outline ~= nil, "Cannot construct Commands without an Outline instance.") 7 | local self = { 8 | -- An instance of an Explorer component which Commands delegates to. 9 | outline = outline, 10 | } 11 | 12 | -- returns a list of @Command(s) defined in 'ide.lib.commands' 13 | -- 14 | -- @return: @table, an array of @Command(s) which export an Outline's 15 | -- command set. 16 | function self.get() 17 | local commands = { 18 | libcmd.new( 19 | libcmd.KIND_ACTION, 20 | "OutlineFocus", 21 | "Focus", 22 | self.outline.focus, 23 | { desc = "Open and focus the Outline." } 24 | ), 25 | libcmd.new( 26 | libcmd.KIND_ACTION, 27 | "OutlineHide", 28 | "Hide", 29 | self.outline.hide, 30 | { desc = "Hide the outline in its current panel. Use Focus to unhide." } 31 | ), 32 | libcmd.new( 33 | libcmd.KIND_ACTION, 34 | "OutlineExpand", 35 | "Expand", 36 | self.outline.expand, 37 | { desc = "Expand the symbol under the current cursor." } 38 | ), 39 | libcmd.new( 40 | libcmd.KIND_ACTION, 41 | "OutlineCollapse", 42 | "Collapse", 43 | self.outline.collapse, 44 | { desc = "Collapse the symbol under the current cursor." } 45 | ), 46 | libcmd.new( 47 | libcmd.KIND_ACTION, 48 | "OutlineCollapseAll", 49 | "CollapseAll", 50 | self.outline.collapse_all, 51 | { desc = "Collapse the symbol under the current cursor." } 52 | ), 53 | libcmd.new( 54 | libcmd.KIND_ACTION, 55 | "OutlineMinimize", 56 | "Minimize", 57 | self.outline.minimize, 58 | { desc = "Minimize the outline window in its panel." } 59 | ), 60 | libcmd.new( 61 | libcmd.KIND_ACTION, 62 | "OutlineMaximize", 63 | "Maximize", 64 | self.outline.maximize, 65 | { desc = "Maximize the outline window in its panel." } 66 | ), 67 | } 68 | return commands 69 | end 70 | 71 | return self 72 | end 73 | 74 | return Commands 75 | -------------------------------------------------------------------------------- /lua/ide/components/callhierarchy/commands.lua: -------------------------------------------------------------------------------- 1 | local libcmd = require("ide.lib.commands") 2 | 3 | local Commands = {} 4 | 5 | Commands.new = function(callhierarchy) 6 | assert(callhierarchy ~= nil, "Cannot construct Commands without an CallHierarchyComponent instance.") 7 | local self = { 8 | -- An instance of an Explorer component which Commands delegates to. 9 | callhierarchy = callhierarchy, 10 | } 11 | 12 | -- returns a list of @Command(s) defined in 'ide.lib.commands' 13 | -- 14 | -- @return: @table, an array of @Command(s) which export an Outline's 15 | -- command set. 16 | function self.get() 17 | local commands = { 18 | libcmd.new( 19 | libcmd.KIND_ACTION, 20 | "CallHierarchyFocus", 21 | "Focus", 22 | self.callhierarchy.focus, 23 | { desc = "Open and focus the CallHierarchy." } 24 | ), 25 | libcmd.new( 26 | libcmd.KIND_ACTION, 27 | "CallHierarchyHide", 28 | "Hide", 29 | self.callhierarchy.hide, 30 | { desc = "Hide the callhierarchy in its current panel. Use Focus to unhide." } 31 | ), 32 | libcmd.new( 33 | libcmd.KIND_ACTION, 34 | "CallHierarchyExpand", 35 | "Expand", 36 | self.callhierarchy.expand, 37 | { desc = "Expand the symbol under the current cursor." } 38 | ), 39 | libcmd.new( 40 | libcmd.KIND_ACTION, 41 | "CallHierarchyCollapse", 42 | "Collapse", 43 | self.callhierarchy.collapse, 44 | { desc = "Collapse the symbol under the current cursor." } 45 | ), 46 | libcmd.new( 47 | libcmd.KIND_ACTION, 48 | "CallHierarchyCollapseAll", 49 | "CollapseAll", 50 | self.callhierarchy.collapse_all, 51 | { desc = "Collapse the symbol under the current cursor." } 52 | ), 53 | libcmd.new( 54 | libcmd.KIND_ACTION, 55 | "CallHierarchyMinimize", 56 | "Minimize", 57 | self.callhierarchy.minimize, 58 | { desc = "Minimize the callhierarchy window in its panel." } 59 | ), 60 | libcmd.new( 61 | libcmd.KIND_ACTION, 62 | "CallHierarchyMaximize", 63 | "Maximize", 64 | self.callhierarchy.maximize, 65 | { desc = "Maximize the CallHierarchy window in its panel." } 66 | ), 67 | libcmd.new( 68 | libcmd.KIND_ACTION, 69 | "CallHierarchyIncomingCalls", 70 | "IncomingCalls", 71 | self.callhierarchy.incoming_calls, 72 | { desc = "Show the incoming call hierarchy for the symbol under the cursor." } 73 | ), 74 | libcmd.new( 75 | libcmd.KIND_ACTION, 76 | "CallHierarchyOutgoingCalls", 77 | "OutgoingCalls", 78 | self.callhierarchy.outgoing_calls, 79 | { desc = "Show the outgoing call hierarchy for the symbol under the cursor." } 80 | ), 81 | } 82 | return commands 83 | end 84 | 85 | return self 86 | end 87 | 88 | return Commands 89 | -------------------------------------------------------------------------------- /lua/ide/components/branches/branchnode.lua: -------------------------------------------------------------------------------- 1 | local node = require("ide.trees.node") 2 | local icons = require("ide.icons") 3 | local gitutil = require("ide.lib.git.client") 4 | local git = require("ide.lib.git.client").new() 5 | local libpopup = require("ide.lib.popup") 6 | local libwin = require("ide.lib.win") 7 | 8 | local BranchNode = {} 9 | 10 | BranchNode.new = function(sha, branch, is_head, depth) 11 | -- extends 'ide.trees.Node' fields. 12 | 13 | local key = string.format("%s:%s", sha, branch) 14 | local self = node.new("git_branch", branch, key, depth) 15 | 16 | self.sha = sha 17 | self.branch = branch 18 | self.is_head = is_head 19 | 20 | if self.depth == 0 then 21 | self.expanded = true 22 | end 23 | 24 | -- Marshal a branchnode into a buffer line. 25 | -- 26 | -- @return: @icon - @string, icon used for call hierarchy item 27 | -- @name - @string, the name of the call hierarchy item 28 | -- @details - @string, the details of the call hierarchy item 29 | function self.marshal() 30 | local icon = icons.global_icon_set.get_icon("GitBranch") 31 | local name = string.format("%s", self.branch) 32 | local detail = "" 33 | 34 | -- root is the file we are displaying the timeline for. 35 | if self.depth == 0 then 36 | icon = icons.global_icon_set.get_icon("GitRepo") 37 | return icon, name, detail 38 | end 39 | 40 | if self.is_head then 41 | name = "* " .. name 42 | end 43 | 44 | if self.remote_ref ~= nil and self.remote_ref ~= "" then 45 | detail = self.remote_ref 46 | else 47 | detail = "~untracked" 48 | end 49 | 50 | if self.tracking ~= "" then 51 | if self.tracking == "ahead" then 52 | detail = detail .. " ↑" 53 | end 54 | if self.tracking == "behind" then 55 | detail = detail .. " ↓" 56 | end 57 | if self.tracking == "diverged" then 58 | detail = detail .. " ↑" .. "↓" 59 | end 60 | end 61 | 62 | return icon, name, detail, "" 63 | end 64 | 65 | local popup_win = nil 66 | function self.details() 67 | if libwin.win_is_valid(popup_win) then 68 | vim.api.nvim_set_current_win(popup_win) 69 | return 70 | end 71 | 72 | git.log(self.sha, 1, function(commits) 73 | local commit = commits[1] 74 | local lines = {} 75 | local head = " " 76 | if self.is_head then 77 | head = "(HEAD)" 78 | end 79 | local remote_ref = self.remote_ref 80 | if self.remote_ref == nil and self.remote_ref == "" then 81 | remote_ref = "~untracked" 82 | end 83 | table.insert( 84 | lines, 85 | string.format("%s %s %7s [%s] %s", head, self.branch, gitutil.short_sha(self.sha), remote_ref, commit.subject) 86 | ) 87 | popup_win = libpopup.until_cursor_move(lines) 88 | end) 89 | end 90 | 91 | return self 92 | end 93 | 94 | return BranchNode 95 | -------------------------------------------------------------------------------- /lua/ide/components/terminal/terminalbrowser/commands.lua: -------------------------------------------------------------------------------- 1 | local libcmd = require('ide.lib.commands') 2 | 3 | local Commands = {} 4 | 5 | Commands.new = function(terminalbrowser) 6 | assert(terminalbrowser ~= nil, "Cannot construct Commands without an TerminalBrowser instance.") 7 | local self = { 8 | -- An instance of an Explorer component which Commands delegates to. 9 | terminalbrowser = terminalbrowser, 10 | } 11 | 12 | -- returns a list of @Command(s) defined in 'ide.lib.commands' 13 | -- 14 | -- @return: @table, an array of @Command(s) which export an TerminalBrowser's 15 | -- command set. 16 | function self.get() 17 | local commands = { 18 | libcmd.new( 19 | libcmd.KIND_ACTION, 20 | "TerminalBrowserFocus", 21 | "Focus", 22 | self.terminalbrowser.focus, 23 | { desc = "Open and focus the TerminalBrowser." } 24 | ), 25 | libcmd.new( 26 | libcmd.KIND_ACTION, 27 | "TerminalBrowserHide", 28 | "Hide", 29 | self.terminalbrowser.hide, 30 | { desc = "Hide the terminal in its current panel. Use Focus to unhide." } 31 | ), 32 | libcmd.new( 33 | libcmd.KIND_ACTION, 34 | "TerminalBrowserMinimize", 35 | "Minimize", 36 | self.terminalbrowser.minimize, 37 | { desc = "Minimize the terminal window in its panel." } 38 | ), 39 | libcmd.new( 40 | libcmd.KIND_ACTION, 41 | "TerminalBrowserMaximize", 42 | "Maximize", 43 | self.terminalbrowser.maximize, 44 | { desc = "Maximize the terminal window in its panel." } 45 | ), 46 | libcmd.new( 47 | libcmd.KIND_ACTION, 48 | "TerminalBrowserNew", 49 | "New", 50 | self.terminalbrowser.new_term, 51 | { desc = "Open a new terminal" } 52 | ), 53 | libcmd.new( 54 | libcmd.KIND_ACTION, 55 | "TerminalBrowserRename", 56 | "Rename", 57 | self.terminalbrowser.rename_term, 58 | { desc = "Rename (or set) a terminal's name" } 59 | ), 60 | libcmd.new( 61 | libcmd.KIND_ACTION, 62 | "TerminalBrowserDelete", 63 | "Delete", 64 | self.terminalbrowser.delete_term, 65 | { desc = "Rename (or set) a terminal's name" } 66 | ), 67 | } 68 | return commands 69 | end 70 | 71 | return self 72 | end 73 | 74 | return Commands 75 | -------------------------------------------------------------------------------- /lua/ide/logger/logger.lua: -------------------------------------------------------------------------------- 1 | local Logger = {} 2 | 3 | Logger.session_id = string.format("nvim-ide-log://%s-%s", "nvim-ide", vim.fn.rand()) 4 | 5 | Logger.log_level = vim.log.levels.INFO 6 | 7 | Logger.buffer = (function() 8 | local buf = vim.api.nvim_create_buf(false, true) 9 | vim.api.nvim_buf_set_name(buf, Logger.session_id) 10 | return buf 11 | end)() 12 | 13 | Logger.set_log_level = function(level) 14 | if level == 'debug' then 15 | Logger.log_level = vim.log.levels.DEBUG 16 | return 17 | end 18 | if level == 'warn' then 19 | Logger.log_level = vim.log.levels.WARN 20 | return 21 | end 22 | if level == 'info' then 23 | Logger.log_level = vim.log.levels.INFO 24 | return 25 | end 26 | if level == 'error' then 27 | Logger.log_level = vim.log.levels.ERROR 28 | return 29 | end 30 | Logger.log_level = vim.log.levels.INFO 31 | end 32 | 33 | function Logger.open_log() 34 | vim.cmd("tabnew") 35 | vim.api.nvim_win_set_buf(0, Logger.buffer) 36 | end 37 | 38 | Logger.new = function(subsys, component) 39 | assert(subsys ~= nil, "Cannot construct a Logger without a subsys field.") 40 | local self = { 41 | -- the subsystem for this logger instance 42 | subsys = subsys, 43 | -- the component within the subsystem producing the log. 44 | component = "", 45 | } 46 | if component ~= nil then 47 | self.component = component 48 | end 49 | 50 | local function _log(level, fmt, ...) 51 | local arg = { ... } 52 | local str = string.format("[%s] [%s] [%s] [%s]: ", os.date("!%Y-%m-%dT%H:%M:%S"), level, self.subsys, self.component) 53 | if arg ~= nil then 54 | str = str .. string.format(fmt, unpack(arg)) 55 | else 56 | str = str .. string.format(fmt) 57 | end 58 | local lines = vim.fn.split(str, "\n") 59 | vim.api.nvim_buf_set_lines(Logger.buffer, -1, -1, false, lines) 60 | end 61 | 62 | function self.error(fmt, ...) 63 | if vim.log.levels.ERROR >= Logger.log_level then 64 | _log("error", fmt, ...) 65 | end 66 | end 67 | 68 | function self.warning(fmt, ...) 69 | if vim.log.levels.WARN >= Logger.log_level then 70 | _log("warning", fmt, ...) 71 | end 72 | end 73 | 74 | function self.info(fmt, ...) 75 | if vim.log.levels.INFO >= Logger.log_level then 76 | _log("info", fmt, ...) 77 | end 78 | end 79 | 80 | function self.debug(fmt, ...) 81 | if vim.log.levels.DEBUG >= Logger.log_level then 82 | _log("debug", fmt, ...) 83 | end 84 | end 85 | 86 | function self.logger_from(subsys, component) 87 | local cur_subsys = self.subsys 88 | if subsys ~= nil then 89 | cur_subsys = subsys 90 | end 91 | local cur_comp = self.component 92 | if component ~= nil then 93 | cur_comp = component 94 | end 95 | return Logger.new(cur_subsys, cur_comp) 96 | end 97 | 98 | return self 99 | end 100 | 101 | return Logger 102 | -------------------------------------------------------------------------------- /lua/ide/components/timeline/commands.lua: -------------------------------------------------------------------------------- 1 | local libcmd = require('ide.lib.commands') 2 | 3 | local Commands = {} 4 | 5 | Commands.new = function(callhierarchy) 6 | assert(callhierarchy ~= nil, "Cannot construct Commands without an CallHierarchyComponent instance.") 7 | local self = { 8 | -- An instance of an Explorer component which Commands delegates to. 9 | callhierarchy = callhierarchy, 10 | } 11 | 12 | -- returns a list of @Command(s) defined in 'ide.lib.commands' 13 | -- 14 | -- @return: @table, an array of @Command(s) which export an Outline's 15 | -- command set. 16 | function self.get() 17 | local commands = { 18 | libcmd.new( 19 | libcmd.KIND_ACTION, 20 | "CallHierarchyFocus", 21 | "Focus", 22 | self.callhierarchy.focus, 23 | { desc = "Open and focus the CallHierarchy." } 24 | ), 25 | libcmd.new( 26 | libcmd.KIND_ACTION, 27 | "CallHierarchyHide", 28 | "Hide", 29 | self.callhierarchy.hide, 30 | { desc = "Hide the callhierarchy in its current panel. Use Focus to unhide." } 31 | ), 32 | libcmd.new( 33 | libcmd.KIND_ACTION, 34 | "CallHierarchyExpand", 35 | "Expand", 36 | self.callhierarchy.expand, 37 | { desc = "Expand the symbol under the current cursor." } 38 | ), 39 | libcmd.new( 40 | libcmd.KIND_ACTION, 41 | "CallHierarchyCollapse", 42 | "Collapse", 43 | self.callhierarchy.collapse, 44 | { desc = "Collapse the symbol under the current cursor." } 45 | ), 46 | libcmd.new( 47 | libcmd.KIND_ACTION, 48 | "CallHierarchyCollapseAll", 49 | "CollapseAll", 50 | self.callhierarchy.collapse_all, 51 | { desc = "Collapse the symbol under the current cursor." } 52 | ), 53 | libcmd.new( 54 | libcmd.KIND_ACTION, 55 | "CallHierarchyMinimize", 56 | "Minimize", 57 | self.callhierarchy.minimize, 58 | { desc = "Minimize the callhierarchy window in its panel." } 59 | ), 60 | libcmd.new( 61 | libcmd.KIND_ACTION, 62 | "CallHierarchyMaximize", 63 | "Maximize", 64 | self.callhierarchy.maximize, 65 | { desc = "Maximize the CallHierarchy window in its panel." } 66 | ), 67 | libcmd.new( 68 | libcmd.KIND_ACTION, 69 | "CallHierarchyIncomingCalls", 70 | "IncomingCalls", 71 | self.callhierarchy.incoming_calls, 72 | { desc = "Show the incoming call hierarchy for the symbol under the cursor." } 73 | ), 74 | libcmd.new( 75 | libcmd.KIND_ACTION, 76 | "CallHierarchyOutgoingCalls", 77 | "OutgoingCalls", 78 | self.callhierarchy.outgoing_calls, 79 | { desc = "Show the outgoing call hierarchy for the symbol under the cursor." } 80 | ), 81 | } 82 | return commands 83 | end 84 | 85 | return self 86 | end 87 | 88 | return Commands 89 | -------------------------------------------------------------------------------- /lua/ide/config.lua: -------------------------------------------------------------------------------- 1 | -- default components 2 | local bufferlist = require("ide.components.bufferlist") 3 | local explorer = require("ide.components.explorer") 4 | local outline = require("ide.components.outline") 5 | local callhierarchy = require("ide.components.callhierarchy") 6 | local timeline = require("ide.components.timeline") 7 | local terminal = require("ide.components.terminal") 8 | local terminalbrowser = require("ide.components.terminal.terminalbrowser") 9 | local changes = require("ide.components.changes") 10 | local commits = require("ide.components.commits") 11 | local branches = require("ide.components.branches") 12 | local bookmarks = require("ide.components.bookmarks") 13 | 14 | local M = {} 15 | 16 | -- The default config which will be merged with the `config` provided to setup() 17 | -- 18 | -- The user provided config may just provide the values they'd like to override 19 | -- and can omit any defaults. 20 | -- 21 | -- Modules can read from this field to get config values. For example 22 | -- require('ide').config. 23 | M.config = { 24 | -- The global icon set to use. 25 | -- values: "nerd", "codicon", "default" 26 | icon_set = "default", 27 | -- Set the log level for nvim-ide's log. Log can be accessed with 28 | -- 'Workspace OpenLog'. Values are 'debug', 'warn', 'info', 'error' 29 | log_level = "info", 30 | -- Component specific configurations and default config overrides. 31 | components = { 32 | -- The global keymap is applied to all Components before construction. 33 | -- It allows common keymaps such as "hide" to be overriden, without having 34 | -- to make an override entry for all Components. 35 | -- 36 | -- If a more specific keymap override is defined for a specific Component 37 | -- this takes precedence. 38 | global_keymaps = { 39 | -- example, change all Component's hide keymap to "h" 40 | -- hide = h 41 | }, 42 | -- example, prefer "x" for hide only for Explorer component. 43 | -- Explorer = { 44 | -- keymaps = { 45 | -- hide = "x", 46 | -- } 47 | -- } 48 | }, 49 | -- default panel groups to display on left and right. 50 | panels = { 51 | left = "explorer", 52 | right = "git", 53 | }, 54 | -- panels defined by groups of components, user is free to redefine the defaults 55 | -- and/or add additional. 56 | panel_groups = { 57 | explorer = { 58 | bufferlist.Name, 59 | explorer.Name, 60 | outline.Name, 61 | callhierarchy.Name, 62 | bookmarks.Name, 63 | terminalbrowser.Name, 64 | }, 65 | terminal = { terminal.Name }, 66 | git = { changes.Name, commits.Name, timeline.Name, branches.Name }, 67 | }, 68 | -- workspaces config 69 | workspaces = { 70 | -- which panels to open by default, one of: 'left', 'right', 'both', 'none' 71 | auto_open = "left", 72 | -- How nvim-ide should handle a ":q" on the last regular (non nvim-ide, non popup) window. 73 | -- "close" - when the last window is closed perform a ":wqa!", closing nvim-ide 74 | -- panels as well 75 | -- "block" - block the last window from closing by creating a new split. 76 | -- nvim-ide panels must be hidden before closing the last window. 77 | -- "disabled" - take no action, this may result in the panels existing 78 | -- when you actually want to close neovim. 79 | -- this is the default mode for backwards compatibility. 80 | on_quit = "disabled", 81 | }, 82 | -- default panel sizes 83 | panel_sizes = { 84 | left = 30, 85 | right = 30, 86 | bottom = 15, 87 | }, 88 | } 89 | 90 | return M 91 | -------------------------------------------------------------------------------- /lua/ide/components/explorer/prompts.lua: -------------------------------------------------------------------------------- 1 | local Prompts = {} 2 | 3 | -- A prompt which asks the user if they want to overwrite the file located at 4 | -- @path. 5 | -- 6 | -- @path - string, the path which should be either renamed or overwritten. 7 | -- @file_op - function(path, overwrite), an abstracted file operation where 8 | -- `path` is the file path being operated on and `overwrite` is whether 9 | -- the operation is performed despite `path` existing. 10 | function Prompts.should_overwrite(path, file_op) 11 | local file = vim.fn.fnamemodify(path, ":t") 12 | vim.ui.input({ 13 | prompt = string.format("%s exists, overwrite or rename (leave blank to cancel): ", file), 14 | default = file, 15 | }, function(input) 16 | if input == nil or input == "" then 17 | -- just return, no input means cancel. 18 | return 19 | end 20 | if input == file then 21 | -- received the same input as prompt, indicates perform the 22 | -- overwrite. 23 | file_op(path, true) 24 | return 25 | end 26 | -- input is new, rewrite path and call file_op 27 | local basepath = vim.fn.fnamemodify(path, ":h") 28 | local to = basepath .. "/" .. input 29 | file_op(to, false) 30 | end) 31 | end 32 | 33 | -- A prompt which asks the user if they want to rename the file located at 34 | -- @path. 35 | -- 36 | -- Overwriting cannot happen here, and thus only new input is allowed. 37 | -- 38 | -- @path - string, the path which should be either renamed or overwritten. 39 | -- @file_op - function(path), an abstracted file operation where 40 | -- `path` is the file path being operated on. 41 | function Prompts.must_rename(path, file_op) 42 | local file = vim.fn.fnamemodify(path, ":t") 43 | vim.ui.input({ 44 | prompt = string.format("%s exists, must rename or cancel (leave input blank to cancel): ", file), 45 | default = file, 46 | }, function(input) 47 | if input == nil or input == "" then 48 | -- just return, no input means cancel. 49 | return 50 | end 51 | -- received the same input, prompt again 52 | if input == file then 53 | Prompts.must_rename(path, file_op) 54 | return 55 | end 56 | -- input is new, rewrite path and call file_op 57 | local basepath = vim.fn.fnamemodify(path, ":h") 58 | local to = basepath .. "/" .. input 59 | file_op(to) 60 | end) 61 | end 62 | 63 | function Prompts.get_filename(callback) 64 | vim.ui.input({ 65 | prompt = string.format("enter filename: "), 66 | }, function(input) 67 | if input == nil or input == "" then 68 | return 69 | end 70 | callback(input) 71 | end) 72 | end 73 | 74 | function Prompts.get_dirname(callback) 75 | vim.ui.input({ 76 | prompt = string.format("enter filename: "), 77 | }, function(input) 78 | if input == nil or input == "" then 79 | return 80 | end 81 | callback(input) 82 | end) 83 | end 84 | 85 | function Prompts.get_file_rename(original_path, callback) 86 | vim.ui.input({ 87 | prompt = string.format("rename file to: "), 88 | default = vim.fn.fnamemodify(original_path, ":t"), 89 | }, function(input) 90 | callback(input) 91 | end) 92 | end 93 | 94 | function Prompts.should_delete(path, callback) 95 | vim.ui.input({ 96 | prompt = string.format("delete %s? (Y/n): ", vim.fn.fnamemodify(path, ":t")), 97 | }, function(input) 98 | if input:lower() ~= "y" then 99 | return 100 | end 101 | callback() 102 | end) 103 | end 104 | 105 | return Prompts 106 | -------------------------------------------------------------------------------- /lua/ide/icons/icon_set.lua: -------------------------------------------------------------------------------- 1 | local IconSet = {} 2 | 3 | local prototype = { 4 | icons = { 5 | Account = "🗣", 6 | Array = "\\[\\]", 7 | Bookmark = "🔖", 8 | Boolean = "∧", 9 | Calendar = "🗓", 10 | Check = "✓", 11 | CheckAll = "🗸🗸", 12 | Circle = "🞆", 13 | CircleFilled = "●", 14 | CirclePause = "⦷", 15 | CircleSlash = "⊘", 16 | CircleStop = "⦻", 17 | Class = "c", 18 | Code = "{}", 19 | Collapsed = "▶", 20 | Color = "🖌", 21 | Comment = "🗩", 22 | CommentExclaim = "🗩", 23 | Constant = "c", 24 | Constructor = "c", 25 | DiffAdded = "+", 26 | Enum = "Ε", 27 | EnumMember = "Ε", 28 | Event = "🗲", 29 | Expanded = "▼", 30 | Field = "𝐟", 31 | File = "🗀", 32 | Folder = "🗁", 33 | Function = "ƒ", 34 | GitBranch = " ", 35 | GitCommit = "⫰", 36 | GitCompare = "⤄", 37 | GitIssue = "⊙", 38 | GitMerge = "⫰", 39 | GitPullRequest = "⬰", 40 | GitRepo = "🕮", 41 | History = "⟲", 42 | IndentGuide = "│", 43 | IndentGuideEnd = "┕", 44 | Info = "🛈", 45 | Interface = "I", 46 | Key = "", 47 | Keyword = "", 48 | Method = "", 49 | Module = "M", 50 | MultiComment = "🗩", 51 | Namespace = "N", 52 | Notebook = "🕮", 53 | Notification = "🕭", 54 | Null = "∅", 55 | Number = "#", 56 | Object = "{}", 57 | Operator = "O", 58 | Package = "{}", 59 | Pass = "🗸", 60 | PassFilled = "🗸", 61 | Pencil = "", 62 | Property = "🛠", 63 | Reference = "⛉", 64 | RequestChanges = "⨪", 65 | Separator = "•", 66 | Space = " ", 67 | String = [[""]], 68 | Struct = "{}", 69 | Sync = "🗘", 70 | Text = [[""]], 71 | Terminal = "🖳", 72 | TypeParameter = "T", 73 | Unit = "U", 74 | Value = "v", 75 | Variable = "V", 76 | }, 77 | -- Retrieves an icon by name. 78 | -- 79 | -- @name - string, the name of an icon to retrieve. 80 | -- 81 | -- return: string or nil, where string is the requested icon if exists. 82 | get_icon = function(name) end, 83 | -- Returns a table of all registered icons 84 | -- 85 | -- return - table, keys are icon names and values are the icons. 86 | list_icons = function() end, 87 | -- Sets an icon. 88 | -- 89 | -- This can add a new icon to the icon set and also overwrite an existing 90 | -- one. 91 | -- 92 | -- returns - void 93 | set_icon = function(name, icon) end, 94 | } 95 | 96 | IconSet.new = function() 97 | local self = vim.deepcopy(prototype) 98 | 99 | function self.get_icon(name) 100 | return self.icons[name] 101 | end 102 | 103 | function self.list_icons() 104 | return self.icons 105 | end 106 | 107 | function self.set_win_highlights() 108 | for name, icon in pairs(self.list_icons()) do 109 | if name == "IndentGuide" then 110 | vim.cmd(string.format("syn match %s /%s/", 'Conceal', icon)) 111 | goto continue 112 | end 113 | 114 | local hi = string.format("%s%s", "TS", name) 115 | if vim.fn.hlexists(hi) ~= 0 then 116 | vim.cmd(string.format("syn match %s /%s/", hi, icon)) 117 | goto continue 118 | end 119 | hi = string.format("%s", name) 120 | if vim.fn.hlexists(hi) ~= 0 then 121 | vim.cmd(string.format("syn match %s /%s/", hi, icon)) 122 | goto continue 123 | end 124 | hi = "Title" 125 | vim.cmd(string.format("syn match %s /%s/", hi, icon)) 126 | ::continue:: 127 | end 128 | end 129 | 130 | function self.set_icon(name, icon) 131 | self.icons[name] = icon 132 | end 133 | 134 | return self 135 | end 136 | 137 | return IconSet 138 | -------------------------------------------------------------------------------- /lua/ide/components/outline/symbolnode.lua: -------------------------------------------------------------------------------- 1 | local node = require("ide.trees.node") 2 | local icons = require("ide.icons") 3 | local liblsp = require("ide.lib.lsp") 4 | local libpopup = require("ide.lib.popup") 5 | local libwin = require("ide.lib.win") 6 | 7 | local SymbolNode = {} 8 | 9 | SymbolNode.new = function(document_symbol, depth) 10 | -- extends 'ide.trees.Node' fields. 11 | 12 | -- range should not be nil per LSP spec, but some LSPs will return nil 13 | -- range, if so fill it in so we can create a unique key. 14 | if document_symbol.range == nil then 15 | document_symbol.range = { 16 | start = { 17 | line = 1, 18 | character = 1, 19 | }, 20 | ["end"] = { 21 | line = 1, 22 | character = 1, 23 | }, 24 | } 25 | end 26 | 27 | local key = 28 | string.format("%s:%s:%s", document_symbol.name, document_symbol.range["start"], document_symbol.range["end"]) 29 | local self = node.new("symbol", document_symbol.name, key, depth) 30 | 31 | if depth == 0 then 32 | self.expanded = true 33 | else 34 | self.expanded = false 35 | end 36 | 37 | -- clear the child's field of document_symbol, it'll be duplicate info once 38 | -- this node is in a @Tree. 39 | local symbol = vim.deepcopy(document_symbol) 40 | symbol.children = (function() 41 | return {} 42 | end)() 43 | 44 | self.document_symbol = symbol 45 | 46 | local popup_win = nil 47 | function self.details() 48 | if libwin.win_is_valid(popup_win) then 49 | vim.api.nvim_set_current_win(popup_win) 50 | return 51 | end 52 | 53 | local icon = " " 54 | local kind = vim.lsp.protocol.SymbolKind[self.document_symbol.kind] 55 | if kind ~= "" then 56 | icon = icons.global_icon_set.get_icon(kind) or "[" .. kind .. "]" 57 | end 58 | 59 | local range = liblsp.get_document_symbol_range(self.document_symbol) 60 | 61 | local lines = {} 62 | table.insert(lines, string.format("%s %s %s", icon, "Symbol:", self.document_symbol.name)) 63 | table.insert(lines, string.format("%s %s %s", icon, "Kind:", kind)) 64 | if range ~= nil then 65 | table.insert( 66 | lines, 67 | string.format( 68 | "%s %s L%d:%d", 69 | icons.global_icon_set.get_icon("File"), 70 | "Start:", 71 | range.start.line, 72 | range.start.character 73 | ) 74 | ) 75 | table.insert( 76 | lines, 77 | string.format( 78 | "%s %s L%d:%d", 79 | icons.global_icon_set.get_icon("File"), 80 | "End:", 81 | range["end"].line, 82 | range["end"].character 83 | ) 84 | ) 85 | end 86 | table.insert(lines, string.format("")) 87 | if (self.document_symbol.detail ~= nil) then 88 | table.insert(lines, string.format("%s", self.document_symbol.detail)) 89 | end 90 | 91 | popup_win = libpopup.until_cursor_move(lines) 92 | end 93 | 94 | -- Marshal a symbolnode into a buffer line. 95 | -- 96 | -- @return: @icon - @string, icon for symbol's kind 97 | -- @name - @string, symbol's name 98 | -- @details - @string, symbol's detail if exists. 99 | function self.marshal() 100 | local icon = "s" 101 | local kind = vim.lsp.protocol.SymbolKind[self.document_symbol.kind] 102 | if kind ~= "" then 103 | icon = icons.global_icon_set.get_icon(kind) or "[" .. kind .. "]" 104 | end 105 | 106 | local name = self.document_symbol.name 107 | local detail = "" 108 | if self.document_symbol.detail ~= nil then 109 | detail = self.document_symbol.detail 110 | end 111 | 112 | return icon, name, detail 113 | end 114 | 115 | return self 116 | end 117 | 118 | return SymbolNode 119 | -------------------------------------------------------------------------------- /lua/ide/components/bookmarks/bookmarknode.lua: -------------------------------------------------------------------------------- 1 | local node = require("ide.trees.node") 2 | local icons = require("ide.icons") 3 | local libpopup = require("ide.lib.popup") 4 | local libwin = require("ide.lib.win") 5 | 6 | local BookmarkNode = {} 7 | 8 | local RECORD_SEP = "␞" 9 | local GROUP_SEP = "␝" 10 | 11 | function BookmarkNode.unmarshal_text(line) 12 | local parts = vim.fn.split(line, RECORD_SEP) 13 | if #parts ~= 5 then 14 | error("line does not contain enough bookmark records") 15 | end 16 | local bm = BookmarkNode.new(parts[1], parts[2], parts[3], parts[4], parts[5]) 17 | -- When we are marhsalling from a line, its assumed we are reading directly 18 | -- from a file, so dirty is false and record the original_start_line to 19 | -- determine if we are back to the disk's representation if the bookmark moves. 20 | bm.dirty = false 21 | bm.original_start_line = bm.start_line 22 | return bm 23 | end 24 | 25 | BookmarkNode.new = function(file, start_line, end_line, title, note, depth) 26 | local key = string.format("%s:%d:%s", file, tonumber(start_line), title) 27 | local self = node.new("bookmark", title, key, depth) 28 | 29 | -- the path, relative to the project's root, which this bookmark was created 30 | -- in. 31 | self.file = file 32 | -- The starting line where the bookmark was created 33 | self.start_line = tonumber(start_line) 34 | -- Used to help determine if a bookmark node is dirty, when its start_line 35 | -- matches its original, it can be un-marked as dirty. 36 | self.original_start_line = nil 37 | -- The ending line where the bookmark was created. 38 | self.end_line = tonumber(end_line) 39 | -- The title of the bookmark 40 | self.title = title 41 | -- Additional note data (no new lines). 42 | self.note = note 43 | -- Whether this bookmark's in-memory representation matches its on-disk. 44 | -- If dirty, it should eventually be written to disk if the file is as well. 45 | self.dirty = true 46 | -- If the bookmark has been displayed in a buffer, this field is set with a 47 | -- a tuple of {bufnr, extmark_id, ns_id} 48 | self.mark = nil 49 | 50 | -- Marshal a bookmarknode into a buffer line. 51 | -- 52 | -- @return: @icon - @string, icon for bookmark's kind 53 | -- @name - @string, bookmark's name 54 | -- @details - @string, bookmark's detail if exists. 55 | function self.marshal() 56 | local icon = icons.global_icon_set.get_icon("Bookmark") 57 | if self.depth == 0 then 58 | icon = icons.global_icon_set.get_icon("Notebook") 59 | end 60 | local name = self.title 61 | if self.depth ~= 0 then 62 | if self.dirty then 63 | name = "*" .. name 64 | end 65 | end 66 | local details = string.format("%s:%d:%d", vim.fn.fnamemodify(self.file, ":t"), self.start_line, self.end_line) 67 | if self.depth == 0 then 68 | details = "" 69 | end 70 | return icon, name, details 71 | end 72 | 73 | -- Marshal this bookmarknode to a line of text suitable for encoding a bookmark 74 | -- into a notebook file. 75 | function self.marshal_text() 76 | return string.format( 77 | "%s%s%d%s%d%s%s%s%s\n", 78 | self.file, 79 | RECORD_SEP, 80 | self.start_line, 81 | RECORD_SEP, 82 | self.end_line, 83 | RECORD_SEP, 84 | self.title, 85 | RECORD_SEP, 86 | self.note 87 | ) 88 | end 89 | 90 | local popup_win = nil 91 | function self.details() 92 | if libwin.win_is_valid(popup_win) then 93 | vim.api.nvim_set_current_win(popup_win) 94 | return 95 | end 96 | 97 | local lines = {} 98 | 99 | table.insert(lines, string.format("%s %s", icons.global_icon_set.get_icon("Bookmark"), self.title)) 100 | table.insert( 101 | lines, 102 | string.format("%s %s", icons.global_icon_set.get_icon("File"), self.file .. ":" .. self.start_line) 103 | ) 104 | 105 | popup_win = libpopup.until_cursor_move(lines) 106 | end 107 | 108 | return self 109 | end 110 | 111 | return BookmarkNode 112 | -------------------------------------------------------------------------------- /lua/ide/buffers/buffer.lua: -------------------------------------------------------------------------------- 1 | local options = require("ide.lib.options") 2 | local libbuf = require("ide.lib.buf") 3 | 4 | local Buffer = {} 5 | 6 | -- Buffer is a base buffer class which can be used to derive more complex buffer 7 | -- types. 8 | -- 9 | -- It provides the basic functionality for a vim buffer, including, reading, 10 | -- writing, ext marks, highlighting, and virtual text. 11 | Buffer.new = function(buf, listed, scratch) 12 | local self = { 13 | buf = nil, 14 | first_write = true, 15 | } 16 | if listed == nil then 17 | listed = true 18 | end 19 | if scratch == nil then 20 | scratch = false 21 | end 22 | if buf == nil then 23 | self.buf = vim.api.nvim_create_buf(listed, scratch) 24 | else 25 | self.buf = buf 26 | end 27 | 28 | local function _modifiable_with_restore() 29 | local restore = function() end 30 | if not self.is_modifiable() then 31 | self.set_modifiable(true) 32 | restore = function() 33 | self.set_modifiable(false) 34 | end 35 | end 36 | return restore 37 | end 38 | 39 | function self.set_modifiable(bool) 40 | if not libbuf.buf_is_valid(self.buf) then 41 | error("attempted to set invalid buffer as modifiable") 42 | end 43 | vim.api.nvim_buf_set_option(self.buf, "modifiable", bool) 44 | end 45 | 46 | function self.is_modifiable() 47 | if not libbuf.buf_is_valid(self.buf) then 48 | return 49 | end 50 | return vim.api.nvim_buf_get_option(self.buf, "modifiable") 51 | end 52 | 53 | function self.truncate() 54 | local restore = _modifiable_with_restore() 55 | libbuf.truncate_buffer(self.buf) 56 | restore() 57 | end 58 | 59 | -- write lines to the buffer. 60 | -- 61 | -- if the buffer is not modifiable it will be set so, and then set back to 62 | -- non-modifiable after the write. 63 | -- 64 | -- if no opts field is provided, this will append lines to the buffer. 65 | -- 66 | -- @lines - @table, an array of @string(s), each of which is a line to write 67 | -- into the buffer. 68 | -- @opts - @table, a table of options to provide to nvim_buf_get_lines 69 | -- Fields: 70 | -- @start - @int, the start line (zero indexed) to begin writing/replacing lines. 71 | -- @end - @int, where to end the write. 72 | -- @strict - @bool, whether to produce an error if start,end are out 73 | -- of the buffer's bound. 74 | -- 75 | -- return: void 76 | function self.write_lines(lines, opts) 77 | if not libbuf.buf_is_valid(self.buf) then 78 | return 79 | end 80 | local restore = _modifiable_with_restore() 81 | local buf_end = vim.api.nvim_buf_line_count(self.buf) 82 | local o = { start = buf_end, ["end"] = buf_end + #lines - 1, strict = false } 83 | if self.first_write then 84 | o.start = 0 85 | end 86 | o = options.merge(o, opts) 87 | vim.api.nvim_buf_set_lines(self.buf, o.start, o["end"], o.strict, lines) 88 | if self.first_write then 89 | self.first_write = false 90 | end 91 | restore() 92 | end 93 | 94 | -- read lines from the buffer. 95 | -- 96 | -- @opts - @table, a table of options to provide to nvim_buf_get_lines 97 | -- Fields: 98 | -- @start - @int, the start line (zero indexed) to begin reading from 99 | -- @end - @int, where to end the read. 100 | -- @strict - @bool, whether to produce an error if start,end are out 101 | -- of the buffer's bound. 102 | -- 103 | -- return: @table, an array of @string, each one being a line from the buffer. 104 | function self.read_lines(opts) 105 | if not libbuf.buf_is_valid(self.buf) then 106 | return 107 | end 108 | local buf_end = vim.api.nvim_buf_line_count(self.buf) 109 | local o = { start = 0, ["end"] = buf_end, strict = false } 110 | o = options.merge(o, opts) 111 | local lines = vim.api.nvim_buf_get_lines(self.buf, o.start, o["end"], o.strict) 112 | return lines 113 | end 114 | 115 | function self.set_name(name) 116 | vim.api.nvim_buf_set_name(self.buf, name) 117 | end 118 | 119 | return self 120 | end 121 | 122 | return Buffer 123 | -------------------------------------------------------------------------------- /lua/ide/trees/node.lua: -------------------------------------------------------------------------------- 1 | local logger = require("ide.logger.logger") 2 | 3 | local Node = {} 4 | 5 | -- A Node is an interface for derived Nodes to implement. 6 | -- 7 | -- A Node is a node within a @Tree and has polymorphic properties. 8 | -- 9 | -- Nodes should be implemented based on the application's needs. 10 | -- 11 | -- @type - the type of the node, required. 12 | -- @name - the name of the node, required 13 | -- @key - a unique key representing this node. 14 | -- @depth - the node's depth, useful if an external caller is building a tree 15 | -- manually. 16 | Node.new = function(type, name, key, depth) 17 | assert(type ~= nil, "cannot construct a Node without a type") 18 | assert(name ~= nil, "cannot construct a Node without a name") 19 | local self = { 20 | -- A potentially non-unique display name for the node. 21 | name = name, 22 | -- The depth of the node when it exists in a @Tree. 23 | depth = nil, 24 | -- A unique identifier for the node in a @Tree. 25 | key = nil, 26 | -- The parent of the Node. 27 | parent = nil, 28 | -- A list of self-similar Node(s) forming a tree structure. 29 | children = {}, 30 | -- When present in a tree, whether the children nodes are accessible or not. 31 | expanded = true, 32 | -- Allows polymorphic behavior of a Node, allowing the type to be identified 33 | type = type, 34 | -- If marshalled, the line within the marshalled buffer this node was 35 | -- written to. 36 | line = nil, 37 | -- The tree the node belongs to 38 | tree = nil, 39 | -- a default logger that's set on construction. 40 | -- a derived class can set this logger instance and base class methods will 41 | -- derive a new logger from it to handle base class component level log fields. 42 | logger = logger.new("trees"), 43 | } 44 | if key ~= nil then 45 | self.key = key 46 | end 47 | if depth ~= nil then 48 | self.depth = depth 49 | end 50 | 51 | -- Marshal will marshal the Node into a buffer line. 52 | -- The function must return three objects to adhere to this interface. 53 | -- 54 | -- returns: 55 | -- @icon - An icon used to represent the Node in a UI. 56 | -- @name - A display name used as the primary identifier of the node in the UI. 57 | -- @detail - A single line, displayed as virtual text, providing further details 58 | -- about the Node. 59 | -- @guide - An override for the expand guide, if present, it will be used 60 | -- regardless of node's expanded field. 61 | -- 62 | -- Returning empty strings for any of the above is valid and will result 63 | -- in the item no being displayed in the buffer line. 64 | self.marshal = function() 65 | error("method must be implemented") 66 | end 67 | 68 | -- Remove a child by its key. 69 | -- 70 | -- @key - string, a unique key of the Node's child to remove. 71 | self.remove_child = function(key) 72 | local new_children = {} 73 | for _, c in ipairs(self.children) do 74 | if c.key ~= key then 75 | table.insert(new_children, c) 76 | end 77 | end 78 | self.children = (function() 79 | return {} 80 | end)() 81 | self.children = new_children 82 | end 83 | 84 | -- Return a Node's child by its key. 85 | -- 86 | -- @key - string, the unique key of the child node to return. 87 | -- return: 88 | -- @Node | nil - the child node if found, nil if not. 89 | self.get_child = function(key) 90 | for _, c in ipairs(self.children) do 91 | if c.key == key then 92 | return c 93 | end 94 | end 95 | return nil 96 | end 97 | 98 | -- A function called to add children to a node on node expansion. 99 | -- 100 | -- This is completely optional and implementation dependent. 101 | -- 102 | -- If an expand function is implemented it *must* use the self.tree 103 | -- reference to add children, or if building the tree manually, recomputing 104 | -- the depth table. 105 | -- 106 | -- This is a bit of an advanced use case so look at examples before 107 | -- implementing. 108 | -- 109 | -- @opts - arbitrary table of options, implementation dependent. 110 | self.expand = function(opts) 111 | -- optionally implemented, no-op otherwise. 112 | end 113 | 114 | return self 115 | end 116 | 117 | return Node 118 | -------------------------------------------------------------------------------- /lua/ide/components/terminal/component.lua: -------------------------------------------------------------------------------- 1 | local base = require("ide.panels.component") 2 | local commands = require("ide.components.terminal.commands") 3 | local logger = require("ide.logger.logger") 4 | local libwin = require("ide.lib.win") 5 | local libbuf = require("ide.lib.buf") 6 | 7 | local TerminalComponent = {} 8 | 9 | local config_prototype = { 10 | default_height = nil, 11 | shell = nil, 12 | disabled_keymaps = false, 13 | keymaps = {}, 14 | } 15 | 16 | TerminalComponent.new = function(name, config) 17 | local self = base.new(name) 18 | 19 | self.config = vim.deepcopy(config_prototype) 20 | 21 | self.terminals = {} 22 | 23 | self.counter = 1 24 | 25 | self.current_term = nil 26 | 27 | self.hidden = true 28 | 29 | self.shell = self.config.shell 30 | if self.shell == nil then 31 | self.shell = vim.fn.getenv("SHELL") 32 | if self.shell == nil or self.shell == "" then 33 | error("could not determine shell to use.") 34 | end 35 | end 36 | 37 | function self.open() 38 | if #self.terminals == 0 then 39 | return vim.api.nvim_create_buf(false, true) 40 | end 41 | return self.terminals[1].buf 42 | end 43 | 44 | -- implements @Component interface 45 | function self.post_win_create() 46 | local buf = vim.api.nvim_win_get_buf(0) 47 | local term = self.get_term_by_buf(buf) 48 | if term == nil then 49 | return 50 | end 51 | libwin.set_winbar_title(0, string.format("Terminal - %s", term.name)) 52 | end 53 | 54 | function self.get_commands() 55 | log = self.logger.logger_from(nil, "Component.get_commands") 56 | return commands.new(self).get() 57 | end 58 | 59 | local opts = { noremap = true, silent = true } 60 | local function terminal_buf_setup(buf) 61 | vim.api.nvim_buf_set_keymap(buf, "t", "n", "", opts) 62 | vim.api.nvim_buf_set_keymap(buf, "t", "h", "h", opts) 63 | vim.api.nvim_buf_set_keymap(buf, "t", "j", "j", opts) 64 | vim.api.nvim_buf_set_keymap(buf, "t", "k", "k", opts) 65 | vim.api.nvim_buf_set_keymap(buf, "t", "l", "l", opts) 66 | vim.api.nvim_buf_set_option(buf, "modifiable", true) 67 | end 68 | 69 | function self.new_term(name, command) 70 | local buf = vim.api.nvim_create_buf(false, true) 71 | terminal_buf_setup(buf) 72 | 73 | if name == nil then 74 | name = self.shell 75 | end 76 | if command == nil then 77 | command = self.shell 78 | end 79 | 80 | term = { 81 | id = self.counter, 82 | buf = buf, 83 | name = name, 84 | } 85 | table.insert(self.terminals, term) 86 | 87 | -- focus ourselves so we can attach the new term 88 | self.focus() 89 | 90 | vim.api.nvim_win_set_buf(0, buf) 91 | vim.fn.termopen(command) 92 | self.set_term(term.id) 93 | vim.cmd("wincmd =") 94 | 95 | self.current_term = self.counter 96 | self.counter = self.counter + 1 97 | return term 98 | end 99 | 100 | function self.delete_term(id) 101 | local term = self.get_term(id) 102 | if term == nil then 103 | return 104 | end 105 | 106 | -- subtlety, do this before deleting the buffer so the TermClose aucmd 107 | -- TerminalBrowser uses doesn't try to delete the term as well 108 | -- (it will be missing from the inventory already.) 109 | local terms = {} 110 | for _, t in ipairs(self.terminals) do 111 | if t.id ~= id then 112 | table.insert(terms, t) 113 | end 114 | end 115 | self.terminals = (function() 116 | return {} 117 | end)() 118 | self.terminals = terms 119 | 120 | vim.api.nvim_buf_set_option(term.buf, "modified", false) 121 | vim.api.nvim_buf_delete(term.buf, { force = true }) 122 | end 123 | 124 | function self.get_terms() 125 | return self.terminals 126 | end 127 | 128 | function self.get_term(id) 129 | for _, term in ipairs(self.terminals) do 130 | if id == term.id then 131 | return term 132 | end 133 | end 134 | end 135 | 136 | function self.get_term_by_buf(buf) 137 | for _, term in ipairs(self.terminals) do 138 | if buf == term.buf then 139 | return term 140 | end 141 | end 142 | end 143 | 144 | function self.set_term(id) 145 | local term = self.get_term(id) 146 | if term == nil then 147 | return 148 | end 149 | 150 | self.focus() 151 | vim.api.nvim_win_set_buf(self.win, term.buf) 152 | libwin.set_winbar_title(0, string.format("Terminal - %s", term.name)) 153 | end 154 | 155 | return self 156 | end 157 | 158 | return TerminalComponent 159 | -------------------------------------------------------------------------------- /lua/ide/workspaces/commands.lua: -------------------------------------------------------------------------------- 1 | local panel = require('ide.panels.panel') 2 | local libcmd = require('ide.lib.commands') 3 | 4 | local Commands = {} 5 | 6 | Commands.new = function(ws) 7 | local self = { 8 | -- The workspace which this Commands structure delegates to. 9 | workspace = ws, 10 | } 11 | 12 | -- Retrieves all commands exported by the Workspaces modules. 13 | -- 14 | -- These three items can be directly used directly with nvim_create_user_command 15 | -- or invoked directly by a caller. 16 | -- 17 | -- return: @table, a command description containing the following fields: 18 | -- @shortname - @string, A name used when displayed by a subcommand 19 | -- @name - @string, A unique name of the command used outside the context 20 | -- of a sub command 21 | -- @callback - @function(args), A callback function which implements 22 | -- the command, args a table described in ":h nvim_create_user_command()" 23 | -- @opts - @table, the options table as described in ":h nvim_create_user_command()" 24 | function self.get() 25 | local commands = { 26 | libcmd.new( 27 | libcmd.KIND_ACTION, 28 | "WorkspaceLeftPanelToggle", 29 | "LeftPanelToggle", 30 | function(_) ws.toggle_panel(panel.PANEL_POS_LEFT) end, 31 | { desc = "Toggles the top panel in the current workspace."} 32 | ), 33 | libcmd.new( 34 | libcmd.KIND_ACTION, 35 | "WorkspaceRightPanelToggle", 36 | "RightPanelToggle", 37 | function(_) ws.toggle_panel(panel.PANEL_POS_RIGHT) end, 38 | { desc = "Toggles the top panel in the current workspace."} 39 | ), 40 | libcmd.new( 41 | libcmd.KIND_ACTION, 42 | "WorkspaceBottomPanelToggle", 43 | "BottomPanelToggle", 44 | function(_) ws.toggle_panel(panel.PANEL_POS_BOTTOM) end, 45 | { desc = "Toggles the top panel in the current workspace."} 46 | ), 47 | libcmd.new( 48 | libcmd.KIND_ACTION, 49 | "WorkspaceLeftPanelClose", 50 | "LeftPanelClose", 51 | function(_) ws.close_panel(panel.PANEL_POS_LEFT) end, 52 | { desc = "Closes the left panel in the current workspace."} 53 | ), 54 | libcmd.new( 55 | libcmd.KIND_ACTION, 56 | "WorkspaceRightPanelClose", 57 | "RightPanelClose", 58 | function(_) ws.close_panel(panel.PANEL_POS_RIGHT) end, 59 | { desc = "Closes the right panel in the current workspace."} 60 | ), 61 | libcmd.new( 62 | libcmd.KIND_ACTION, 63 | "WorkspaceBottomPanelClose", 64 | "BottomPanelClose", 65 | function(_) ws.close_panel(panel.PANEL_POS_BOTTOM) end, 66 | { desc = "Closes the bottom panel in the current workspace."} 67 | ), 68 | libcmd.new( 69 | libcmd.KIND_ACTION, 70 | "WorkspaceMaximizeComponent", 71 | "MaximizeComponent", 72 | function(_) ws.maximize_component() end, 73 | { desc = "Maximize the component your cursor is in."} 74 | ), 75 | libcmd.new( 76 | libcmd.KIND_ACTION, 77 | "WorkspaceReset", 78 | "Reset", 79 | function(_) ws.equal_components() end, 80 | { desc = "Set all component windows to equal sizes or their user specified default heights"} 81 | ), 82 | libcmd.new( 83 | libcmd.KIND_ACTION, 84 | "WorkspaceSwapPanel", 85 | "SwapPanel", 86 | function(_) ws.select_swap_panel() end, 87 | { desc = "Swap a panel with another panel group."} 88 | ), 89 | libcmd.new( 90 | libcmd.KIND_ACTION, 91 | "WorkspaceOpenLog", 92 | "OpenLog", 93 | function(_) require('ide.logger.logger').open_log() end, 94 | { desc = "Open nvim-ide's log in a new tab."} 95 | ), 96 | } 97 | return commands 98 | end 99 | 100 | return self 101 | end 102 | 103 | 104 | return Commands 105 | -------------------------------------------------------------------------------- /lua/ide/components/commits/commitnode.lua: -------------------------------------------------------------------------------- 1 | local node = require("ide.trees.node") 2 | local icons = require("ide.icons") 3 | local logger = require("ide.logger.logger") 4 | local git = require("ide.lib.git.client").new() 5 | local libpopup = require("ide.lib.popup") 6 | local libwin = require("ide.lib.win") 7 | 8 | local CommitNode = {} 9 | 10 | CommitNode.new = function(sha, file, subject, author, date, tags, depth) 11 | -- extends 'ide.trees.Node' fields. 12 | 13 | local key = string.format("%s:%s:%s:%s:%s", sha, file, subject, author, date) 14 | local self = node.new("git_commit", sha, sha, depth) 15 | 16 | -- CommitNodes make a list, not a tree, so just always expand and we'll set 17 | -- the tree to marshal with no leave guides. 18 | self.expanded = true 19 | self.sha = sha 20 | self.file = file 21 | self.subject = subject 22 | self.author = author 23 | self.date = date 24 | self.tags = tags 25 | self.is_file = false 26 | self.is_head = false 27 | 28 | -- all nodes start as collapsed. 29 | self.expanded = false 30 | if self.depth == 0 then 31 | self.expanded = true 32 | end 33 | 34 | -- Marshal a commitnode into a buffer line. 35 | -- 36 | -- @return: @icon - @string, icon used for call hierarchy item 37 | -- @name - @string, the name of the call hierarchy item 38 | -- @details - @string, the details of the call hierarchy item 39 | function self.marshal() 40 | local icon = icons.global_icon_set.get_icon("GitCommit") 41 | if self.author == "" then 42 | icon = icons.global_icon_set.get_icon("File") 43 | end 44 | if self.depth == 0 then 45 | icon = icons.global_icon_set.get_icon("GitRepo") 46 | end 47 | 48 | local name = string.format("%s", self.subject) 49 | if self.is_head and self.depth ~= 0 and not self.is_file then 50 | name = "* " .. name 51 | end 52 | local detail = string.format("%s %s", self.author, self.date) 53 | if self.tags then 54 | detail = string.format("%s %s %s", self.tags, self.author, self.date) 55 | end 56 | if self.is_file then 57 | return icon, name, detail, "" 58 | end 59 | 60 | return icon, name, detail 61 | end 62 | 63 | local popup_win = nil 64 | function self.details(tab) 65 | if libwin.win_is_valid(popup_win) then 66 | vim.api.nvim_set_current_win(popup_win) 67 | return 68 | end 69 | 70 | git.log(self.sha, 1, function(data) 71 | if data == nil then 72 | return 73 | end 74 | 75 | local commit = data[1] 76 | if commit == nil then 77 | return 78 | end 79 | 80 | local lines = {} 81 | 82 | if self.is_file then 83 | table.insert(lines, string.format("%s %s", icons.global_icon_set.get_icon("File"), self.file)) 84 | end 85 | 86 | table.insert(lines, string.format("%s %s", icons.global_icon_set.get_icon("GitCommit"), commit.sha)) 87 | if (self.tags) then 88 | table.insert(lines, string.format("%s%s", icons.global_icon_set.get_icon("GitCommit"), self.tags)) 89 | end 90 | table.insert(lines, string.format("%s %s", icons.global_icon_set.get_icon("Account"), commit.author)) 91 | table.insert(lines, string.format("%s %s", icons.global_icon_set.get_icon("Calendar"), commit.date)) 92 | table.insert(lines, "") 93 | 94 | local subject = vim.fn.split(commit.subject, "\n") 95 | table.insert(lines, subject[1]) 96 | 97 | table.insert(lines, "") 98 | 99 | local body = vim.fn.split(commit.body, "\n") 100 | for _, l in ipairs(body) do 101 | table.insert(lines, l) 102 | end 103 | 104 | if tab then 105 | vim.cmd("tabnew") 106 | local buf = vim.api.nvim_get_current_buf() 107 | vim.api.nvim_buf_set_option(buf, "buftype", "nofile") 108 | vim.api.nvim_buf_set_lines(buf, 0, #lines, false, lines) 109 | vim.api.nvim_buf_set_option(buf, "modifiable", false) 110 | vim.api.nvim_buf_set_name(buf, commit.sha .. " details") 111 | return 112 | end 113 | 114 | libpopup.until_cursor_move(lines) 115 | 116 | popup_win = libpopup.until_cursor_move(lines) 117 | end) 118 | end 119 | 120 | function self.expand(cb) 121 | if self.is_file then 122 | return 123 | end 124 | git.show_rev_paths(self.sha, function(paths) 125 | if self.depth == 0 then 126 | self.expanded = true 127 | return 128 | end 129 | local children = {} 130 | for _, path in ipairs(paths) do 131 | local file = CommitNode.new(path.rev, path.path, path.path, "", "") 132 | file.is_file = true 133 | table.insert(children, file) 134 | end 135 | self.tree.add_node(self, children) 136 | self.expanded = true 137 | if cb ~= nil then 138 | cb() 139 | end 140 | end) 141 | end 142 | 143 | return self 144 | end 145 | 146 | return CommitNode 147 | -------------------------------------------------------------------------------- /lua/ide/trees/tree_test.lua: -------------------------------------------------------------------------------- 1 | local tree = require("ide.trees.tree") 2 | local node = require("ide.trees.node") 3 | 4 | local M = {} 5 | 6 | local test_cases = { 7 | { 8 | "▼ + test_root", 9 | " ▼ - test_c1", 10 | " ▼ - test_c2", 11 | }, 12 | { 13 | "▼ + test_root", 14 | " ▼ - test_c1", 15 | " ▼ - test_c1.1", 16 | " ▼ - test_c2", 17 | }, 18 | { 19 | "▼ - test_c1", 20 | " ▼ - test_c1.1", 21 | }, 22 | { 23 | "▼ - test_c1", 24 | " ▼ - test_c1.1", 25 | " ▼ - test_c1.1.1", 26 | }, 27 | { 28 | "▶ - test_c1", 29 | }, 30 | { 31 | "▼ - test_c1", 32 | " ▼ - test_c1.1", 33 | " ▼ - test_c1.1.1", 34 | }, 35 | { 36 | "▶ - test_c1", 37 | }, 38 | { 39 | "▼ - test_c1", 40 | " ▶ - test_c1.1", 41 | }, 42 | { 43 | "▼ - test_c1", 44 | " ▼ - test_c1.1", 45 | " - test_c1.1.1", 46 | }, 47 | { 48 | " - test_c1", 49 | " - test_c1.1", 50 | " - test_c1.1.1", 51 | }, 52 | { 53 | "▼ - test_c1", 54 | }, 55 | } 56 | 57 | local function check_tc(n, buf) 58 | for i, l in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do 59 | local tc = test_cases[n] 60 | assert(tc[i] == l, "expected lines to match" .. "\n" .. tc[i] .. "\n" .. l) 61 | end 62 | end 63 | 64 | function M.test_functionality() 65 | -- marshal a basic 3 node tree. 66 | local root = node.new("test", "test_root", "test_root", 0) 67 | root.marshal = function() 68 | return "+", "test_root", "root node" 69 | end 70 | local c1 = node.new("test", "test_c1", "test_c1") 71 | c1.marshal = function() 72 | return "-", "test_c1", "test child 1" 73 | end 74 | local c2 = node.new("test", "test_c2", "test_c2") 75 | c2.marshal = function() 76 | return "-", "test_c2", "test child 2" 77 | end 78 | local t = tree.new("test") 79 | t.add_node(root, { c1, c2 }) 80 | local buf = vim.api.nvim_create_buf(true, false) 81 | t.buffer = buf 82 | t.marshal() 83 | 84 | check_tc(1, buf) 85 | 86 | -- add a child to c1 node 87 | local c1_1 = node.new("test", "test_c1.1", "test_c1.1") 88 | c1_1.marshal = function() 89 | return "-", "test_c1.1", "test child 1.1" 90 | end 91 | t.add_node(c1, { c1_1 }) 92 | t.marshal() 93 | check_tc(2, buf) 94 | 95 | -- reparent tree to test_c1 96 | t.reparent_node(c1) 97 | t.marshal() 98 | check_tc(3, buf) 99 | 100 | -- add a child to c1.1 101 | local c1_1_1 = node.new("test", "test_c1.1.1", "test_c1.1.1") 102 | c1_1_1.marshal = function() 103 | return "-", "test_c1.1.1", "test child 1.1.1" 104 | end 105 | t.add_node(c1_1, { c1_1_1 }) 106 | t.marshal() 107 | check_tc(4, buf) 108 | 109 | -- collapse c1 110 | t.collapse_node(c1) 111 | t.marshal() 112 | check_tc(5, buf) 113 | 114 | -- expand c1 115 | t.expand_node(c1) 116 | t.marshal() 117 | check_tc(6, buf) 118 | 119 | -- recursive collapse subtree 120 | t.collapse_subtree(c1) 121 | t.marshal() 122 | check_tc(7, buf) 123 | 124 | -- expand, we expect the entire subtree to be collapsed. 125 | t.expand_node(c1) 126 | t.marshal() 127 | check_tc(8, buf) 128 | 129 | -- ensure a leaf node does not have an expand guide. 130 | t.expand_node(c1_1) 131 | t.marshal({ 132 | no_guides = false, 133 | no_guides_leaf = true, 134 | icon_set = require("ide.icons.icon_set").new(), 135 | }) 136 | check_tc(9, buf) 137 | 138 | -- ensure no guides are shown. 139 | t.expand_node(c1_1) 140 | t.marshal({ 141 | no_guides = true, 142 | no_guides_leaf = true, 143 | icon_set = require("ide.icons.icon_set").new(), 144 | }) 145 | check_tc(10, buf) 146 | 147 | -- ensure search key finds node. 148 | node = t.search_key(c1_1.key) 149 | assert(node.key == c1_1.key, "search did not locate c1_1 node.") 150 | 151 | -- ensure walk works 152 | local seen = {} 153 | t.walk_subtree(t.root, function(n) 154 | seen[n.key] = n 155 | return true 156 | end) 157 | assert(seen[c1.key] ~= nil, "node c1 was not seen in walk") 158 | assert(seen[c1_1.key] ~= nil, "node c1 was not seen in walk") 159 | assert(seen[c1_1_1.key] ~= nil, "node c1 was not seen in walk") 160 | 161 | -- ensure walk works from an arbitrary node. 162 | seen = (function() 163 | return {} 164 | end)() 165 | t.walk_subtree(c1_1, function(n) 166 | seen[n.key] = n 167 | return true 168 | end) 169 | assert(seen[c1.key] == nil, "node c1 was incorrectly seen in walk") 170 | assert(seen[c1_1.key] ~= nil, "node c1 was not seen in walk") 171 | assert(seen[c1_1_1.key] ~= nil, "node c1 was not seen in walk") 172 | 173 | -- ensure remove_subtree removes a subtree 174 | t.remove_subtree(c1) 175 | t.marshal() 176 | check_tc(11, buf) 177 | 178 | -- test unmarshal 179 | node = t.unmarshal(1) 180 | assert(node.key == c1.key, "expected line 1 to unmarshal to c1 node") 181 | end 182 | 183 | return M 184 | -------------------------------------------------------------------------------- /lua/ide/components/callhierarchy/callnode.lua: -------------------------------------------------------------------------------- 1 | local node = require('ide.trees.node') 2 | local icons = require('ide.icons') 3 | local liblsp = require('ide.lib.lsp') 4 | local prompts = require('ide.components.explorer.prompts') 5 | local logger = require('ide.logger.logger') 6 | 7 | local CallNode = {} 8 | 9 | CallNode.new = function(component, direction, call_hierarchy_item_call, depth) 10 | -- extends 'ide.trees.Node' fields. 11 | 12 | local call_hierarchy_item = liblsp.call_hierarchy_call_to_item(direction, call_hierarchy_item_call) 13 | assert(call_hierarchy_item ~= nil, "could not extract a CallHierarchyItem from the provided CallHierarchyCall") 14 | 15 | -- range should not be nil per LSP spec, but some LSPs will return nil 16 | -- range, if so fill it in so we can create a unique key. 17 | if call_hierarchy_item.range == nil then 18 | call_hierarchy_item.range = { 19 | start = { 20 | line = 1, 21 | character = 1 22 | }, 23 | ['end'] = { 24 | line = 1, 25 | character = 1 26 | } 27 | } 28 | end 29 | 30 | local key = string.format("%s:%s:%s", call_hierarchy_item.name, call_hierarchy_item.range["start"], 31 | call_hierarchy_item.range["end"]) 32 | 33 | local self = node.new(direction .. "_call_hierarchy", call_hierarchy_item.name, key, depth) 34 | 35 | -- important, the base class defaults to nodes being expanded on creation. 36 | -- we don't want this for CallNodes, since we dynamically fill a CallNode's 37 | -- children on expand. 38 | self.expanded = false 39 | -- keep a reference to the component which created self, we'll reuse the 40 | -- method set. 41 | self.component = component 42 | -- one of "incoming" or "outgoing", the orientation of this node. 43 | self.direction = direction 44 | -- the result from a "callHierarchy/*Calls" LSP request. 45 | -- see: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#callHierarchy_outgoingCalls 46 | self.call_hierarchy_item_call = call_hierarchy_item_call 47 | 48 | -- Marshal a callnode into a buffer line. 49 | -- 50 | -- @return: @icon - @string, icon used for call hierarchy item 51 | -- @name - @string, the name of the call hierarchy item 52 | -- @details - @string, the details of the call hierarchy item 53 | function self.marshal() 54 | local guide = nil 55 | if self.expanded and #self.children == 0 then 56 | guide = "" 57 | end 58 | 59 | local call_hierarchy_item = liblsp.call_hierarchy_call_to_item(self.direction, self.call_hierarchy_item_call) 60 | local icon = "s" 61 | local kind = vim.lsp.protocol.SymbolKind[call_hierarchy_item.kind] 62 | if kind ~= "" then 63 | icon = icons.global_icon_set.get_icon(kind) or "[" .. kind .. "]" 64 | end 65 | 66 | local name = call_hierarchy_item.name 67 | local detail = "" 68 | if call_hierarchy_item.detail ~= nil then 69 | detail = call_hierarchy_item.detail 70 | end 71 | 72 | return icon, name, detail, guide 73 | end 74 | 75 | -- Expands a callnode. 76 | -- This will perform an additional callHierarchy/* call and and its children 77 | -- to self. 78 | -- 79 | -- @opts - @table, options for the expand, for future use. 80 | -- 81 | -- return: void 82 | function self.expand(opts) 83 | local item = liblsp.call_hierarchy_call_to_item(self.direction, self.call_hierarchy_item_call) 84 | local callback = function(direction, call_hierarchy_item, call_hierarchy_calls) 85 | if call_hierarchy_calls == nil then 86 | self.expanded = true 87 | self.tree.marshal({ }) 88 | self.component.state["cursor"].restore() 89 | return 90 | end 91 | local children = {} 92 | for _, call in ipairs(call_hierarchy_calls) do 93 | table.insert(children, CallNode.new(self.component, direction, call)) 94 | end 95 | self.tree.add_node(self, children) 96 | self.expanded = true 97 | self.tree.marshal({ }) 98 | self.component.state["cursor"].restore() 99 | end 100 | if self.direction == "incoming" then 101 | liblsp.make_incoming_calls_request(item, { client_id = self.component.state["client_id"] }, callback) 102 | return 103 | end 104 | if self.direction == "outgoing" then 105 | liblsp.make_outgoing_calls_request(item, { client_id = self.component.state["client_id"] }, callback) 106 | return 107 | end 108 | end 109 | 110 | return self 111 | end 112 | 113 | return CallNode 114 | -------------------------------------------------------------------------------- /lua/ide/workspaces/component_tracker.lua: -------------------------------------------------------------------------------- 1 | local logger = require("ide.logger.logger") 2 | local libwin = require("ide.lib.win") 3 | 4 | local ComponentTracker = {} 5 | 6 | -- A ComponentTracker is responsible for listening to autocommand events and 7 | -- updating stateful properties of a @Component within a @Panel and storing 8 | -- these updates in the @Component's state field. 9 | ComponentTracker.new = function(workspace) 10 | local self = { 11 | -- the @Workspace to track components for. 12 | -- a table of created autocommands for components being tracked. 13 | active_autocmds = {}, 14 | workspace = workspace, 15 | } 16 | 17 | local function find_component_by_win(win) 18 | if self.workspace.panels.left ~= nil then 19 | for _, c in ipairs(self.workspace.panels.left.components) do 20 | if c.win == win then 21 | return c 22 | end 23 | end 24 | end 25 | if self.workspace.panels.right ~= nil then 26 | for _, c in ipairs(self.workspace.panels.right.components) do 27 | if c.win == win then 28 | return c 29 | end 30 | end 31 | end 32 | if self.workspace.panels.bottom ~= nil then 33 | for _, c in ipairs(self.workspace.panels.bottom.components) do 34 | if c.win == win then 35 | return c 36 | end 37 | end 38 | end 39 | return nil 40 | end 41 | 42 | function self.on_win_resized_event(args, component) 43 | local log = logger.new("workspaces", "ComponentTracker.on_win_resized_event") 44 | log.debug("recording window resizing for each component") 45 | if self.workspace.tab ~= vim.api.nvim_get_current_tabpage() then 46 | log.debug("Not for this workspace, returning.") 47 | return 48 | end 49 | 50 | local function handle_panel(panel) 51 | -- update all components h/w, this is necessary since scrolling a 52 | -- a window does not recursively fire a WinScrolled event for 53 | -- adjacent component windows. 54 | for _, cc in ipairs(panel.components) do 55 | if cc.is_displayed() then 56 | if cc.state["dimensions"] == nil then 57 | cc.state["dimensions"] = {} 58 | end 59 | local h = vim.api.nvim_win_get_height(cc.win) 60 | local w = vim.api.nvim_win_get_width(cc.win) 61 | cc.state["dimensions"].height = h 62 | cc.state["dimensions"].width = w 63 | end 64 | end 65 | end 66 | 67 | handle_panel(self.workspace.panels.left) 68 | handle_panel(self.workspace.panels.right) 69 | handle_panel(self.workspace.panels.bottom) 70 | 71 | log.debug("updated dimensions for components in workspace %s", self.workspace.tab) 72 | end 73 | 74 | -- an autocmd which records the last cursor position along with a restore 75 | -- function. 76 | function self.on_cursor_moved(_, component) 77 | local log = logger.new("panels", "ComponentTracker.on_cursor_moved") 78 | local win = vim.api.nvim_get_current_win() 79 | 80 | -- we will allow the passing in of the component, this is helpful on 81 | -- a call to self.refresh() since we want to populate component state 82 | -- sometimes before the autocmds fire 83 | if component ~= nil and component.win ~= nil then 84 | win = component.win 85 | end 86 | 87 | log.debug("handling cursor moved event ws %d", self.workspace.tab) 88 | local c = find_component_by_win(win) 89 | if c == nil then 90 | log.debug("nil component for win %d, returning", win) 91 | return 92 | end 93 | 94 | local cursor = libwin.get_cursor(win) 95 | c.state["cursor"] = { 96 | cursor = cursor, 97 | -- restore the *current* value of win if possible, this occurs when 98 | -- the component is toggled closed and open. 99 | restore = function() 100 | if not libwin.win_is_valid(c.win) then 101 | return 102 | end 103 | libwin.safe_cursor_restore(c.win, c.state["cursor"].cursor) 104 | end, 105 | } 106 | log.debug("wrote cursor update to component state: cursor [%d,%d]", cursor[1], cursor[2]) 107 | end 108 | 109 | -- used to register autocommands on panel changes, like registering a new 110 | -- component. 111 | function self.refresh() 112 | local log = logger.new("panels", "ComponentTracker.refresh") 113 | log.debug("refreshing component tracker for workspace %d", self.workspace.tab) 114 | 115 | for _, aucmd in ipairs(self.active_autocmds) do 116 | vim.api.nvim_del_autocmd(aucmd.id) 117 | end 118 | 119 | self.active_autocmds = (function() 120 | return {} 121 | end)() 122 | 123 | table.insert(self.active_autocmds, { 124 | id = vim.api.nvim_create_autocmd("WinResized", { 125 | callback = self.on_win_resized_event, 126 | }), 127 | }) 128 | 129 | table.insert(self.active_autocmds, { 130 | id = vim.api.nvim_create_autocmd({ "CursorMoved" }, { 131 | pattern = "component://*", 132 | callback = self.on_cursor_moved, 133 | }), 134 | }) 135 | 136 | self.on_cursor_moved(nil) 137 | self.on_win_resized_event(nil) 138 | end 139 | 140 | function self.stop() end 141 | 142 | return self 143 | end 144 | 145 | return ComponentTracker 146 | -------------------------------------------------------------------------------- /lua/ide/lib/buf.lua: -------------------------------------------------------------------------------- 1 | local Buf = {} 2 | 3 | function Buf.is_in_workspace(buf) 4 | local name = vim.api.nvim_buf_get_name(buf) 5 | -- make it absolute 6 | name = vim.fn.fnamemodify(name, ":p") 7 | local cwd = vim.fn.getcwd() 8 | cwd = vim.fn.fnamemodify(cwd, ":p") 9 | if vim.fn.match(name, cwd) == -1 then 10 | return false 11 | end 12 | return true 13 | end 14 | 15 | function Buf.buf_is_valid(buf) 16 | if buf ~= nil and vim.api.nvim_buf_is_valid(buf) then 17 | return true 18 | end 19 | return false 20 | end 21 | 22 | function Buf.is_component_buf(buf) 23 | local name = vim.api.nvim_buf_get_name(buf) 24 | if vim.fn.match(name, "component://") >= 0 then 25 | return true 26 | end 27 | return false 28 | end 29 | 30 | function Buf.is_regular_buffer(buf) 31 | if not Buf.buf_is_valid(buf) then 32 | return false 33 | end 34 | 35 | -- only consider normal buffers with files loaded into them. 36 | if vim.api.nvim_buf_get_option(buf, "buftype") ~= "" then 37 | return false 38 | end 39 | 40 | local buf_name = vim.api.nvim_buf_get_name(buf) 41 | 42 | -- component buffers are not regular buffers 43 | if string.sub(buf_name, 1, 12) == "component://" then 44 | return false 45 | end 46 | 47 | -- diff buffers are not regular buffers 48 | if string.sub(buf_name, 1, 7) == "diff://" then 49 | return false 50 | end 51 | 52 | return true 53 | end 54 | 55 | function Buf.next_regular_buffer() 56 | for _, buf in ipairs(vim.api.nvim_list_bufs()) do 57 | if (Buf.is_regular_buffer(buf)) then 58 | return buf 59 | end 60 | end 61 | return nil 62 | end 63 | 64 | function Buf.truncate_buffer(buf) 65 | if Buf.buf_is_valid(buf) then 66 | vim.api.nvim_buf_set_lines(buf, 0, -1, false, {}) 67 | end 68 | end 69 | 70 | function Buf.toggle_modifiable(buf) 71 | if Buf.buf_is_valid(buf) then 72 | vim.api.nvim_buf_set_option(buf, "modifiable", true) 73 | return function() 74 | vim.api.nvim_buf_set_option(buf, "modifiable", false) 75 | end 76 | end 77 | return function() end 78 | end 79 | 80 | function Buf.has_lsp_clients(buf) 81 | if #vim.lsp.get_clients({ bufnr = buf }) > 0 then 82 | return true 83 | end 84 | return false 85 | end 86 | 87 | function Buf.set_option_with_restore(buf, option, value) 88 | local cur = vim.api.nvim_buf_get_option(buf, option) 89 | vim.api.nvim_win_set_option(buf, value) 90 | return function() 91 | vim.api.nvim_win_set_option(buf, cur) 92 | end 93 | end 94 | 95 | function Buf.buf_exists_by_name(name) 96 | for _, buf in ipairs(vim.api.nvim_list_bufs()) do 97 | local buf_name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ":.") 98 | if buf_name == name then 99 | return true, buf 100 | end 101 | end 102 | return false 103 | end 104 | 105 | function Buf.delete_buffer_by_name(name) 106 | for _, buf in ipairs(vim.api.nvim_list_bufs()) do 107 | local buf_name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ":.") 108 | if buf_name == name then 109 | vim.api.nvim_buf_delete(buf, { force = true }) 110 | end 111 | end 112 | end 113 | 114 | function Buf.is_listed_buf(bufnr) 115 | return vim.bo[bufnr].buflisted and vim.api.nvim_buf_is_loaded(bufnr) and vim.api.nvim_buf_get_name(bufnr) ~= "" 116 | end 117 | 118 | function Buf.get_listed_bufs() 119 | return vim.tbl_filter(function(bufnr) 120 | return Buf.is_listed_buf(bufnr) 121 | end, vim.api.nvim_list_bufs()) 122 | end 123 | 124 | function Buf.get_current_filenames() 125 | return vim.tbl_map(vim.api.nvim_buf_get_name, Buf.get_listed_bufs()) 126 | end 127 | 128 | function Buf.get_unique_filename(filename) 129 | local filenames = vim.tbl_filter(function(filename_other) 130 | return filename_other ~= filename 131 | end, Buf.get_current_filenames()) 132 | 133 | -- Reverse filenames in order to compare their names 134 | filename = string.reverse(filename) 135 | filenames = vim.tbl_map(string.reverse, filenames) 136 | 137 | local index 138 | 139 | -- For every other filename, compare it with the name of the current file char-by-char to 140 | -- find the minimum index `i` where the i-th character is different for the two filenames 141 | -- After doing it for every filename, get the maximum value of `i` 142 | if next(filenames) then 143 | index = math.max(unpack(vim.tbl_map(function(filename_other) 144 | for i = 1, #filename do 145 | -- Compare i-th character of both names until they aren't equal 146 | if filename:sub(i, i) ~= filename_other:sub(i, i) then 147 | return i 148 | end 149 | end 150 | return 1 151 | end, filenames))) 152 | else 153 | index = 1 154 | end 155 | 156 | -- Iterate backwards (since filename is reversed) until a "/" is found 157 | -- in order to show a valid file path 158 | while index <= #filename do 159 | if filename:sub(index, index) == "/" then 160 | index = index - 1 161 | break 162 | end 163 | 164 | index = index + 1 165 | end 166 | 167 | return string.reverse(string.sub(filename, 1, index)) 168 | end 169 | 170 | -- Opinionated way of setting per-buffer keymaps, this is the typical 171 | -- usage for nvim-ide components. 172 | function Buf.set_keymap_normal(buf, keymap, cb) 173 | vim.api.nvim_buf_set_keymap(buf, "n", keymap, "", { silent = true, callback = cb }) 174 | end 175 | 176 | return Buf 177 | -------------------------------------------------------------------------------- /lua/ide/components/explorer/commands.lua: -------------------------------------------------------------------------------- 1 | local libcmd = require('ide.lib.commands') 2 | 3 | local Commands = {} 4 | 5 | Commands.new = function(ex) 6 | assert(ex ~= nil, "Cannot construct Commands with an Explorer instance.") 7 | local self = { 8 | -- An instance of an Explorer component which Commands delegates to. 9 | explorer = ex, 10 | } 11 | 12 | -- returns a list of @Command(s) defined in 'ide.lib.commands' 13 | -- 14 | -- @return: @table, an array of @Command(s) which export an Explorer's 15 | -- command set. 16 | function self.get() 17 | local commands = { 18 | libcmd.new( 19 | libcmd.KIND_ACTION, 20 | "ExplorerFocus", 21 | "Focus", 22 | self.explorer.focus_with_expand, 23 | { desc = "Open and focus the Explorer." } 24 | ), 25 | libcmd.new( 26 | libcmd.KIND_ACTION, 27 | "ExplorerHide", 28 | "Hide", 29 | self.explorer.hide, 30 | { desc = "Hide the explorer in its current panel. Use Focus to unhide." } 31 | ), 32 | libcmd.new( 33 | libcmd.KIND_ACTION, 34 | "ExplorerExpand", 35 | "Expand", 36 | self.explorer.expand, 37 | { desc = "Expand the directory under the current cursor." } 38 | ), 39 | libcmd.new( 40 | libcmd.KIND_ACTION, 41 | "ExplorerCollapse", 42 | "Collapse", 43 | self.explorer.collapse, 44 | { desc = "Collapse the directory under the current cursor." } 45 | ), 46 | libcmd.new( 47 | libcmd.KIND_ACTION, 48 | "ExplorerCollapseAll", 49 | "CollapseAll", 50 | self.explorer.collapse_all, 51 | { desc = "Collapse all directories up to root." } 52 | ), 53 | libcmd.new( 54 | libcmd.KIND_ACTION, 55 | "ExplorerEdit", 56 | "EditFile", 57 | self.explorer.open_filenode, 58 | { desc = "Open the file for editing under the current cursor." } 59 | ), 60 | libcmd.new( 61 | libcmd.KIND_ACTION, 62 | "ExplorerMinimize", 63 | "Minimize", 64 | self.explorer.minimize, 65 | { desc = "Minimize the explorer window in its panel." } 66 | ), 67 | libcmd.new( 68 | libcmd.KIND_ACTION, 69 | "ExplorerMaximize", 70 | "Maximize", 71 | self.explorer.maximize, 72 | { desc = "Maximize the Explorer window in its panel." } 73 | ), 74 | libcmd.new( 75 | libcmd.KIND_ACTION, 76 | "ExplorerNewFile", 77 | "NewFile", 78 | self.explorer.touch, 79 | { desc = "Create a new file at the current cursor." } 80 | ), 81 | libcmd.new( 82 | libcmd.KIND_ACTION, 83 | "ExplorerNewDir", 84 | "NewDir", 85 | self.explorer.mkdir, 86 | { desc = "Create a new directory at the current cursor." } 87 | ), 88 | libcmd.new( 89 | libcmd.KIND_ACTION, 90 | "ExplorerRename", 91 | "Rename", 92 | self.explorer.rename, 93 | { desc = "Rename the file at the current cursor." } 94 | ), 95 | libcmd.new( 96 | libcmd.KIND_ACTION, 97 | "ExplorerDelete", 98 | "Delete", 99 | self.explorer.rm, 100 | { desc = "Delete the file at the current cursor." } 101 | ), 102 | libcmd.new( 103 | libcmd.KIND_ACTION, 104 | "ExplorerCopy", 105 | "Copy", 106 | self.explorer.cp, 107 | { desc = "(Recurisve) Copy the selected files to the destination under the cursor." } 108 | ), 109 | libcmd.new( 110 | libcmd.KIND_ACTION, 111 | "ExplorerMove", 112 | "Move", 113 | self.explorer.mv, 114 | { desc = "(Recursive) Move the selected files to the destination under the cursor." } 115 | ), 116 | libcmd.new( 117 | libcmd.KIND_ACTION, 118 | "ExplorerSelect", 119 | "Select", 120 | self.explorer.select, 121 | { desc = "Select the file under the cursor for further action." } 122 | ), 123 | libcmd.new( 124 | libcmd.KIND_ACTION, 125 | "ExplorerUnselect", 126 | "Unselect", 127 | self.explorer.unselect, 128 | { desc = "Unselect the file under the cursor for further action." } 129 | ), 130 | } 131 | return commands 132 | end 133 | 134 | return self 135 | end 136 | 137 | return Commands 138 | -------------------------------------------------------------------------------- /lua/ide/lib/lsp.lua: -------------------------------------------------------------------------------- 1 | local LSP = {} 2 | 3 | function LSP.request_all_first_result(resp) 4 | for id, r in pairs(resp) do 5 | if r.result ~= nil and not vim.tbl_isempty(r.result) then 6 | return r.result, id 7 | end 8 | end 9 | return nil 10 | end 11 | 12 | function LSP.call_hierarchy_call_to_item(direction, call) 13 | if direction == "incoming" then 14 | return call["from"] 15 | end 16 | if direction == "outgoing" then 17 | return call["to"] 18 | end 19 | return nil 20 | end 21 | 22 | function LSP.item_to_call_hierarchy_call(direction, item) 23 | if direction == "incoming" then 24 | return { 25 | from = item, 26 | fromRanges = {}, 27 | } 28 | end 29 | if direction == "outgoing" then 30 | return { 31 | to = item, 32 | fromRanges = {}, 33 | } 34 | end 35 | end 36 | 37 | function LSP.make_prepare_call_hierarchy_request(text_document_pos, opts, callback) 38 | if text_document_pos == nil then 39 | text_document_pos = vim.lsp.util.make_position_params() 40 | if text_document_pos == nil then 41 | return 42 | end 43 | end 44 | 45 | local method = "textDocument/prepareCallHierarchy" 46 | 47 | -- perform on current buffer 48 | if opts == nil then 49 | vim.lsp.buf_request_all(0, method, text_document_pos, function(resp) 50 | local call_hierarchy_item, client_id = LSP.request_all_first_result(resp) 51 | callback(call_hierarchy_items, client_id) 52 | end) 53 | return 54 | end 55 | 56 | -- get client and make a request with this 57 | if opts.client_id ~= nil then 58 | local client = vim.lsp.get_client_by_id(opts.client_id) 59 | if client == nil then 60 | return 61 | end 62 | client.request(method, text_document_pos, function(err, call_hierarchy_item, _, _) 63 | if err ~= nil then 64 | error(vim.inspect(err)) 65 | end 66 | callback(call_hierarchy_items) 67 | end) 68 | return 69 | end 70 | 71 | -- perform a request with all active clients for the provided buffer. 72 | if opts.buf ~= nil then 73 | vim.lsp.buf_request_all(opts.buf, method, text_document_pos, function(resp) 74 | local call_hierarchy_items, client_id = LSP.request_all_first_result(resp) 75 | callback(call_hierarchy_items, client_id) 76 | end) 77 | return 78 | end 79 | end 80 | 81 | function LSP.make_call_hierarchy_request(direction, call_hierarchy_item, opts, callback) 82 | local method = nil 83 | if direction == "outgoing" then 84 | method = "callHierarchy/outgoingCalls" 85 | end 86 | if direction == "incoming" then 87 | method = "callHierarchy/incomingCalls" 88 | end 89 | if method == nil then 90 | error("direction must be `incoming` or `outgoing`: direction=" .. direction) 91 | end 92 | assert(call_hierarchy_item ~= nil, "call_hierarchy_item cannot be nil") 93 | 94 | local params = { item = call_hierarchy_item } 95 | 96 | -- perform on current buffer 97 | if opts == nil then 98 | vim.lsp.buf_request_all(0, method, params, function(resp) 99 | local call_hierarchy_calls = LSP.request_all_first_result(resp) 100 | callback(direction, call_hierarchy_item, call_hierarchy_calls) 101 | end) 102 | return 103 | end 104 | 105 | -- get client and make a request with this 106 | if opts.client_id ~= nil then 107 | local client = vim.lsp.get_client_by_id(opts.client_id) 108 | if client == nil then 109 | return 110 | end 111 | client.request(method, params, function(err, call_hierarchy_calls, _, _) 112 | if err ~= nil then 113 | error(vim.inspect(err)) 114 | end 115 | callback(direction, call_hierarchy_item, call_hierarchy_calls) 116 | end) 117 | return 118 | end 119 | 120 | -- perform a request with all active clients for the provided buffer. 121 | if opts.buf ~= nil then 122 | vim.lsp.buf_request_all(opts.buf, method, params, function(resp) 123 | local call_hierarchy_calls = LSP.request_all_first_result(resp) 124 | callback(direction, call_hierarchy_item, call_hierarchy_calls) 125 | end) 126 | return 127 | end 128 | end 129 | 130 | function LSP.make_outgoing_calls_request(call_hierarchy_item, opts, callback) 131 | LSP.make_call_hierarchy_request("outgoing", call_hierarchy_item, opts, callback) 132 | end 133 | 134 | function LSP.make_incoming_calls_request(call_hierarchy_item, opts, callback) 135 | LSP.make_call_hierarchy_request("incoming", call_hierarchy_item, opts, callback) 136 | end 137 | 138 | function LSP.highlight_call_hierarchy_call(buf, direction, call_hierarchy_item_call) 139 | local highlights = {} 140 | local call_hierarchy_item = LSP.call_hierarchy_call_to_item(direction, call_hierarchy_item_call) 141 | if call_hierarchy_item == nil then 142 | return 143 | end 144 | 145 | table.insert(highlights, { range = call_hierarchy_item.selectionRange }) 146 | 147 | for _, range in ipairs(call_hierarchy_item_call.fromRanges) do 148 | table.insert(highlights, { range = range }) 149 | end 150 | 151 | vim.lsp.util.buf_highlight_references(buf, highlights, "utf-8") 152 | 153 | return function() 154 | vim.lsp.util.buf_clear_references(buf) 155 | end 156 | end 157 | 158 | function LSP.get_document_symbol_range(document_symbol) 159 | if document_symbol.selectionRange ~= nil then 160 | return document_symbol.selectionRange 161 | end 162 | if document_symbol.location ~= nil and document_symbol.location["range"] ~= nil then 163 | return document_symbol.location["range"] 164 | end 165 | if document_symbol.range ~= nil then 166 | return document_symbol.selectionRange 167 | end 168 | return nil 169 | end 170 | 171 | function LSP.detach_all_clients_buf(buf) 172 | vim.lsp.for_each_buffer_client(buf, function(_, client_id, bufnr) 173 | vim.lsp.buf_detach_client(bufnr, client_id) 174 | end) 175 | end 176 | 177 | return LSP 178 | -------------------------------------------------------------------------------- /lua/ide/panels/panel_test.lua: -------------------------------------------------------------------------------- 1 | local panel = require("ide.panels.panel") 2 | local tc = require("ide.panels.test_component") 3 | 4 | local M = {} 5 | 6 | function M.test_component_register() 7 | local tp = panel.new(vim.api.nvim_get_current_tabpage(), panel.PANEL_POS_TOP) 8 | if not tp then 9 | assert(false, "expected panel creation to succeed") 10 | end 11 | local lp = panel.new(vim.api.nvim_get_current_tabpage(), panel.PANEL_POS_LEFT) 12 | if not lp then 13 | assert(false, "expected panel creation to succeed") 14 | end 15 | local rp = panel.new(vim.api.nvim_get_current_tabpage(), panel.PANEL_POS_RIGHT) 16 | if not rp then 17 | assert(false, "expected panel creation to succeed") 18 | end 19 | local bp = panel.new(vim.api.nvim_get_current_tabpage(), panel.PANEL_POS_BOTTOM) 20 | if not bp then 21 | assert(false, "expected panel creation to succeed") 22 | end 23 | 24 | tp.register_component(tc.new("top_component_1")) 25 | tp.register_component(tc.new("top_component_2")) 26 | lp.register_component(tc.new("left_component_1")) 27 | lp.register_component(tc.new("left_component_2")) 28 | rp.register_component(tc.new("right_component_1")) 29 | rp.register_component(tc.new("right_component_2")) 30 | bp.register_component(tc.new("bottom_component_1")) 31 | bp.register_component(tc.new("bottom_component_2")) 32 | 33 | tp.open_panel() 34 | lp.open_panel() 35 | rp.open_panel() 36 | bp.open_panel() 37 | end 38 | 39 | function M.test_panel_close() 40 | local tp = panel.new(vim.api.nvim_get_current_tabpage(), panel.PANEL_POS_TOP) 41 | if not tp then 42 | assert(false, "expected panel creation to succeed") 43 | end 44 | local lp = panel.new(vim.api.nvim_get_current_tabpage(), panel.PANEL_POS_LEFT) 45 | if not lp then 46 | assert(false, "expected panel creation to succeed") 47 | end 48 | local rp = panel.new(vim.api.nvim_get_current_tabpage(), panel.PANEL_POS_RIGHT) 49 | if not rp then 50 | assert(false, "expected panel creation to succeed") 51 | end 52 | local bp = panel.new(vim.api.nvim_get_current_tabpage(), panel.PANEL_POS_BOTTOM) 53 | if not bp then 54 | assert(false, "expected panel creation to succeed") 55 | end 56 | 57 | tp.register_component(tc.new("top_component_1")) 58 | tp.register_component(tc.new("top_component_2")) 59 | lp.register_component(tc.new("left_component_1")) 60 | lp.register_component(tc.new("left_component_2")) 61 | rp.register_component(tc.new("right_component_1")) 62 | rp.register_component(tc.new("right_component_2")) 63 | bp.register_component(tc.new("bottom_component_1")) 64 | bp.register_component(tc.new("bottom_component_2")) 65 | 66 | tp.open_panel() 67 | lp.open_panel() 68 | rp.open_panel() 69 | bp.open_panel() 70 | 71 | tp.close_panel() 72 | lp.close_panel() 73 | rp.close_panel() 74 | bp.close_panel() 75 | end 76 | 77 | function M.test_panel_functionality() 78 | -- run this test with another component open, ensuring all things work while 79 | -- a secondary panel is present. 80 | local lp = panel.new(vim.api.nvim_get_current_tabpage(), panel.PANEL_POS_LEFT) 81 | if not lp then 82 | assert(false, "expected left panel creation to succeed") 83 | end 84 | local lc = tc.new("left_component_1") 85 | lp.register_component(lc) 86 | lp.open_panel() 87 | 88 | local tp = panel.new(vim.api.nvim_get_current_tabpage(), panel.PANEL_POS_TOP) 89 | if not tp then 90 | assert(false, "expected top panel creation to succeed") 91 | end 92 | 93 | local c = tc.new("top_component_1") 94 | tp.register_component(c) 95 | 96 | tp.open_panel() 97 | assert(tp.is_open(), "expected is_open to return true.") 98 | 99 | tp.close_panel() 100 | assert(not tp.is_open(), "expected is_open to return false.") 101 | 102 | tp.open_component(c.name) 103 | assert(tp.is_open(), "expected is_open to return true.") 104 | local cur_win = vim.api.nvim_get_current_win() 105 | local cur_buf = vim.api.nvim_get_current_buf() 106 | assert(cur_win == c.win, "expected current win to be component's win") 107 | assert(cur_buf == c.buf, "expected current win to be component's buf") 108 | 109 | tp.hide_component(c.name) 110 | cur_win = vim.api.nvim_get_current_win() 111 | assert(cur_win ~= c.win, "expected current win to be component's win") 112 | assert(c.is_hidden(), "expected component to be in hidden state") 113 | 114 | -- confirm open after hide, unhides the element 115 | tp.open_component(c.name) 116 | assert(tp.is_open(), "expected is_open to return true.") 117 | cur_win = vim.api.nvim_get_current_win() 118 | cur_buf = vim.api.nvim_get_current_buf() 119 | assert(cur_win == c.win, "expected current win to be component's win") 120 | assert(cur_buf == c.buf, "expected current win to be component's buf") 121 | assert(not c.is_hidden(), "expected component to not be in hidden state") 122 | 123 | local c2 = tc.new("top_component_2") 124 | tp.register_component(c2) 125 | -- TODO: it takes an open and a close of the panel to display a newly registered 126 | -- component, consider if this should be done automatically in the register 127 | -- method, somewhere else or not at all. 128 | tp.close_panel() 129 | tp.open_panel() 130 | tp.open_component(c2.name) 131 | cur_win = vim.api.nvim_get_current_win() 132 | cur_buf = vim.api.nvim_get_current_buf() 133 | assert(cur_win == c2.win, "expected current win to be component's win") 134 | assert(cur_buf == c2.buf, "expected current win to be component's buf") 135 | assert(not c2.is_hidden(), "expected component to not be in hidden state") 136 | 137 | tp.hide_component(c2.name) 138 | cur_win = vim.api.nvim_get_current_win() 139 | cur_buf = vim.api.nvim_get_current_buf() 140 | assert(cur_win == c.win, "expected current win to be component's win") 141 | assert(cur_buf == c.buf, "expected current win to be component's buf") 142 | end 143 | 144 | return M 145 | -------------------------------------------------------------------------------- /lua/ide/components/terminal/terminalbrowser/component.lua: -------------------------------------------------------------------------------- 1 | local base = require("ide.panels.component") 2 | local tree = require("ide.trees.tree") 3 | local icons = require("ide.icons") 4 | local libbuf = require("ide.lib.buf") 5 | local termnode = require("ide.components.terminal.terminalbrowser.terminalnode") 6 | local logger = require("ide.logger.logger") 7 | local commands = require("ide.components.terminal.terminalbrowser.commands") 8 | 9 | local TerminalBrowserComponent = {} 10 | 11 | local config_prototype = { 12 | default_height = nil, 13 | disabled_keymaps = false, 14 | hidden = false, 15 | keymaps = { 16 | new = "n", 17 | jump = "", 18 | hide = "H", 19 | delete = "D", 20 | rename = "r", 21 | }, 22 | } 23 | 24 | TerminalBrowserComponent.new = function(name, config) 25 | local self = base.new(name) 26 | self.tree = tree.new("terminals") 27 | -- a logger that will be used across this class and its base class methods. 28 | self.logger = logger.new("terminalbrowser") 29 | 30 | 31 | -- seup config, use default and merge in user config if not nil 32 | self.config = vim.deepcopy(config_prototype) 33 | if config ~= nil then 34 | self.config = vim.tbl_deep_extend("force", config_prototype, config) 35 | end 36 | 37 | self.hidden = self.config.hidden 38 | 39 | local function setup_buffer() 40 | local buf = vim.api.nvim_create_buf(false, true) 41 | 42 | vim.api.nvim_buf_set_option(buf, "bufhidden", "hide") 43 | vim.api.nvim_buf_set_option(buf, "filetype", "filetree") 44 | vim.api.nvim_buf_set_option(buf, "buftype", "nofile") 45 | vim.api.nvim_buf_set_option(buf, "modifiable", false) 46 | vim.api.nvim_buf_set_option(buf, "swapfile", false) 47 | vim.api.nvim_buf_set_option(buf, "textwidth", 0) 48 | vim.api.nvim_buf_set_option(buf, "wrapmargin", 0) 49 | 50 | if not self.config.disable_keymaps then 51 | vim.api.nvim_buf_set_keymap(buf, "n", self.config.keymaps.new, "", { 52 | silent = true, 53 | callback = function() 54 | self.new_term() 55 | end, 56 | }) 57 | vim.api.nvim_buf_set_keymap(buf, "n", self.config.keymaps.rename, "", { 58 | silent = true, 59 | callback = function() 60 | self.rename_term() 61 | end, 62 | }) 63 | vim.api.nvim_buf_set_keymap(buf, "n", self.config.keymaps.jump, "", { 64 | silent = true, 65 | callback = function() 66 | self.jump() 67 | end, 68 | }) 69 | vim.api.nvim_buf_set_keymap(buf, "n", self.config.keymaps.hide, "", { 70 | silent = true, 71 | callback = function() 72 | self.hide() 73 | end, 74 | }) 75 | vim.api.nvim_buf_set_keymap(buf, "n", self.config.keymaps.delete, "", { 76 | silent = true, 77 | callback = function() 78 | self.delete_term() 79 | end, 80 | }) 81 | end 82 | 83 | return buf 84 | end 85 | 86 | self.buf = setup_buffer() 87 | self.tree.set_buffer(self.buf) 88 | 89 | function self.refresh() 90 | local tc = self.workspace.search_component("Terminal") 91 | if tc == nil then 92 | return 93 | end 94 | local terms = tc.component.get_terms() 95 | local children = {} 96 | for _, term in ipairs(terms) do 97 | table.insert(children, termnode.new(term.id, term.name)) 98 | end 99 | local root = termnode.new(0, "terminals", 0) 100 | self.tree.add_node(root, children) 101 | self.tree.marshal({ no_guides = true }) 102 | if self.state["cursor"] ~= nil then 103 | self.state["cursor"].restore() 104 | end 105 | end 106 | 107 | function self.open() 108 | self.refresh() 109 | return self.buf 110 | end 111 | 112 | function self.post_win_create() 113 | local log = self.logger.logger_from(nil, "Component.post_win_create") 114 | icons.global_icon_set.set_win_highlights() 115 | end 116 | 117 | function self.new_term(args, name, command) 118 | local tc = self.workspace.search_component("Terminal") 119 | if tc == nil then 120 | return 121 | end 122 | local term = tc.component.new_term(name, command) 123 | self.refresh() 124 | local aucmd = nil 125 | aucmd = vim.api.nvim_create_autocmd({ "TermClose" }, { 126 | callback = function(e) 127 | if e.buf == term.buf and libbuf.buf_is_valid(e.buf) then 128 | tc.component.delete_term(term.id) 129 | self.refresh() 130 | vim.api.nvim_del_autocmd(aucmd) 131 | end 132 | end, 133 | }) 134 | end 135 | 136 | function self.rename_term(args) 137 | local node = self.tree.unmarshal(self.state["cursor"].cursor[1]) 138 | if node == nil then 139 | return 140 | end 141 | local tc = self.workspace.search_component("Terminal") 142 | if tc == nil then 143 | return 144 | end 145 | vim.ui.input({ 146 | prompt = "Rename terminal to: ", 147 | default = node.name, 148 | }, function(name) 149 | if name == nil then 150 | return 151 | end 152 | local term = tc.component.get_term(node.id) 153 | term.name = name 154 | self.refresh() 155 | tc.component.set_term(term.id) 156 | end) 157 | end 158 | 159 | function self.delete_term(args) 160 | local node = self.tree.unmarshal(self.state["cursor"].cursor[1]) 161 | if node == nil then 162 | return 163 | end 164 | local tc = self.workspace.search_component("Terminal") 165 | if tc == nil then 166 | return 167 | end 168 | tc.component.delete_term(node.id) 169 | self.refresh() 170 | end 171 | 172 | function self.jump(args) 173 | local node = self.tree.unmarshal(self.state["cursor"].cursor[1]) 174 | if node == nil then 175 | return 176 | end 177 | local tc = self.workspace.search_component("Terminal") 178 | if tc == nil then 179 | return 180 | end 181 | tc.component.set_term(node.id) 182 | end 183 | 184 | function self.get_commands() 185 | log = self.logger.logger_from(nil, "Component.get_commands") 186 | return commands.new(self).get() 187 | end 188 | 189 | return self 190 | end 191 | 192 | return TerminalBrowserComponent 193 | -------------------------------------------------------------------------------- /lua/ide/trees/marshaller.lua: -------------------------------------------------------------------------------- 1 | local icon_set = require('ide.icons').global_icon_set 2 | 3 | local Marshaller = {} 4 | 5 | -- A Marshaller is responsible for taking a @Tree and marshalling its @Node(s) 6 | -- into buffer lines, providing the user facing interface for a @Tree. 7 | -- 8 | -- The Marshaller keeps track of which buffer lines associate with which @Node 9 | -- in the @Tree and can be used for quick retrieval of a @Node given a buffer 10 | -- line. 11 | Marshaller.new = function() 12 | local self = { 13 | -- A mapping between marshalled buffer lines and their @Node which the line 14 | -- represents. 15 | buffer_line_mapping = {}, 16 | -- A working array of buffer lines used during marshalling, eventually 17 | -- written out to the @Tree's buffer. 18 | buffer_lines = {}, 19 | -- A working array of virtual text line descriptions, eventually applied to 20 | -- the lines in the @Tree's buffer. 21 | virtual_text_lines = {}, 22 | } 23 | 24 | local function recursive_marshal(node, opts) 25 | local expand_guide = "" 26 | if node.expanded then 27 | expand_guide = icon_set.get_icon("Expanded") 28 | else 29 | expand_guide = icon_set.get_icon("Collapsed") 30 | end 31 | if opts.no_guides then 32 | expand_guide = "" 33 | end 34 | if opts.no_guides_leaf then 35 | if #node.children == 0 then 36 | expand_guide = " " 37 | end 38 | end 39 | 40 | local icon, name, detail, guide = node.marshal() 41 | 42 | -- do some nil checking to be safe 43 | if icon == nil then 44 | icon = " " 45 | end 46 | if name == nil then 47 | name = "" 48 | end 49 | if detail == nil then 50 | detail = "" 51 | end 52 | 53 | -- pad detail a bit 54 | detail = detail .. " " 55 | 56 | if guide ~= nil then 57 | expand_guide = guide 58 | end 59 | 60 | -- add some padding for all non-root nodes 61 | local buffer_line = icon_set.get_icon("Space") 62 | if node.depth == 0 then 63 | buffer_line = "" 64 | end 65 | 66 | for i = 1, node.depth do 67 | if node.depth > 1 and i > 1 then 68 | buffer_line = buffer_line .. icon_set.get_icon('IndentGuide') .. icon_set.get_icon("Space") 69 | goto continue 70 | end 71 | buffer_line = buffer_line .. icon_set.get_icon("Space") 72 | ::continue:: 73 | end 74 | 75 | buffer_line = buffer_line .. expand_guide .. icon_set.get_icon("Space") 76 | buffer_line = buffer_line .. icon .. icon_set.get_icon("Space") .. name 77 | 78 | buffer_line = vim.fn.substitute(buffer_line, "\\n", " ", "g") 79 | table.insert(self.buffer_lines, buffer_line) 80 | self.buffer_line_mapping[#self.buffer_lines] = node 81 | node.line = #self.buffer_lines 82 | table.insert(self.virtual_text_lines, { { detail, "Conceal" } }) 83 | 84 | -- don't recurse if current node is not expanded. 85 | if not node.expanded then 86 | return 87 | end 88 | 89 | for _, c in ipairs(node.children) do 90 | recursive_marshal(c, opts) 91 | end 92 | end 93 | 94 | -- Kicks off the marshalling of the @Tree @Node(s) into the associated @Tree.buffer 95 | -- 96 | -- Once this method completes a text representation of the @Tree will be present 97 | -- in @Tree.buffer. 98 | -- 99 | -- @Tree - @Tree, an @Tree which has an associated and valid @Tree.buffer 100 | -- field. 101 | -- 102 | -- @opts - A table providing options to the marshalling process 103 | -- Fields: 104 | -- no_guides - bool, do not display expand/collapsed guides 105 | -- no_guides_leaf - bool, if the marshaller can determine the 106 | -- node is a leaf, do not marshall an expand/collaped guide. 107 | -- returns: void 108 | function self.marshal(Tree, opts) 109 | if Tree.root == nil then 110 | error("attempted to marshal a tree with a nil root") 111 | end 112 | if Tree.buffer == nil or 113 | (not vim.api.nvim_buf_is_valid(Tree.buffer)) 114 | then 115 | error("attempted to marshal a tree with invalid buffer " .. Tree.buffer) 116 | end 117 | 118 | if opts == nil then 119 | opts = {} 120 | end 121 | local o = { 122 | no_guides = false, 123 | no_guides_leaf = false, 124 | restore = nil, 125 | virt_text_pos = 'eol', 126 | hl_mode = 'combine' 127 | } 128 | o = vim.tbl_extend("force", o, opts) 129 | 130 | -- zero out our bookkeepers 131 | self.buffer_line_mapping = (function() return {} end)() 132 | self.buffer_lines = (function() return {} end)() 133 | self.virtual_text_lines = (function() return {} end)() 134 | 135 | recursive_marshal(Tree.root, o) 136 | 137 | -- recursive marshalling done, now write out buffer lines and apply 138 | -- virtual text. 139 | vim.api.nvim_buf_set_option(Tree.buffer, 'modifiable', true) 140 | vim.api.nvim_buf_set_lines(Tree.buffer, 0, -1, true, {}) 141 | vim.api.nvim_buf_set_lines(Tree.buffer, 0, #self.buffer_lines, false, self.buffer_lines) 142 | vim.api.nvim_buf_set_option(Tree.buffer, 'modifiable', false) 143 | for i, vt in ipairs(self.virtual_text_lines) do 144 | if vt[1][1] == "" then 145 | goto continue 146 | end 147 | local opts = { 148 | virt_text = vt, 149 | virt_text_pos = o.virt_text_pos, 150 | hl_mode = o.hl_mode, 151 | } 152 | vim.api.nvim_buf_set_extmark(Tree.buffer, 1, i - 1, 0, opts) 153 | ::continue:: 154 | end 155 | end 156 | 157 | -- Return the @Node associated with the marshalled line number. 158 | -- 159 | -- @linenr - integer, the marshalled line number to which the @Node should 160 | -- be returned. 161 | -- 162 | -- return: @Node | nil. 163 | function self.unmarshal(linenr) 164 | return self.buffer_line_mapping[linenr] 165 | end 166 | 167 | function self.reset() 168 | self.buffer_line_mapping = (function() return {} end)() 169 | self.buffer_lines = (function() return {} end)() 170 | self.virtual_text_lines = (function() return {} end)() 171 | end 172 | 173 | return self 174 | end 175 | 176 | return Marshaller 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ███╗ ██╗██╗ ██╗██╗███╗ ███╗ ██╗██████╗ ███████╗ 3 | ████╗ ██║██║ ██║██║████╗ ████║ ██║██╔══██╗██╔════╝ 4 | ██╔██╗ ██║██║ ██║██║██╔████╔██║█████╗██║██║ ██║█████╗ 5 | ██║╚██╗██║╚██╗ ██╔╝██║██║╚██╔╝██║╚════╝██║██║ ██║██╔══╝ 6 | ██║ ╚████║ ╚████╔╝ ██║██║ ╚═╝ ██║ ██║██████╔╝███████╗ 7 | ╚═╝ ╚═══╝ ╚═══╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝ 8 | ``` 9 | ![nvim-ide](./contrib/screenshot.png) 10 | 11 |

LazyVim + NVIM-IDE + Github NVIM Theme

12 | 13 | [Workflow Video](https://youtu.be/GcoHnB5DoFA) 14 | 15 | `nvim-ide` is a complete IDE layer for Neovim, heavily inspired by `vscode`. 16 | 17 | It provides a default set of components, an extensible API for defining your 18 | own, IDE-like panels and terminal/utility windows, and the ability to swap between 19 | user defined panels. 20 | 21 | This plugin is for individuals who are looking for a cohesive IDE experience 22 | from Neovim and are less concerned with mixing and matching from the awesome 23 | ecosystem of Neovim plugins. 24 | 25 | The current set of default components include: 26 | * Bookmarks - Per-workspace collections of bookmarks with sticky support. 27 | * Branches - Checkout and administer the workspaces's git branches 28 | * Buffers - Display and administer the currently opened buffers. 29 | * CallHierarchy - Display an LSP's CallHierarchy request in an intuitive tree. 30 | * Changes - Display the current git status and stage/restore/commit/amend the diff. 31 | * Commits - Display the list of commits from HEAD, view a read only diff or checkout a commit and view a modifiable diff. 32 | * Explorer - A file explorer which supports file selection and recursive operations. 33 | * Outline - A real-time LSP powered source code outline supporting jumping and tracking. 34 | * TerminalBrowser - A terminal manager for creating, renaming, jumping-to, and deleting terminal instances. 35 | * Timeline - Displays the git history of a file, showing you how the file was manipulated over several commits. 36 | 37 | We put a lot of efforts into writing `docs/nvim-ide.txt`, so please refer to this 38 | file for introduction, usage, and development information. 39 | 40 | ## Getting started 41 | 42 | **Ensure you have Neovim v0.8.0 or greater.** 43 | 44 | 1. Get the plugin via your favorite plugin manager. 45 | 46 | [Plug](https://github.com/junegunn/vim-plug): 47 | ``` 48 | Plug 'ldelossa/nvim-ide' 49 | ``` 50 | 51 | [Packer.nvim](https://github.com/wbthomason/packer.nvim): 52 | ``` 53 | use { 54 | 'ldelossa/nvim-ide' 55 | } 56 | ``` 57 | 58 | 2. Call the setup function (optionally with the default config): 59 | 60 | ```lua 61 | -- default components 62 | local bufferlist = require('ide.components.bufferlist') 63 | local explorer = require('ide.components.explorer') 64 | local outline = require('ide.components.outline') 65 | local callhierarchy = require('ide.components.callhierarchy') 66 | local timeline = require('ide.components.timeline') 67 | local terminal = require('ide.components.terminal') 68 | local terminalbrowser = require('ide.components.terminal.terminalbrowser') 69 | local changes = require('ide.components.changes') 70 | local commits = require('ide.components.commits') 71 | local branches = require('ide.components.branches') 72 | local bookmarks = require('ide.components.bookmarks') 73 | 74 | require('ide').setup({ 75 | -- The global icon set to use. 76 | -- values: "nerd", "codicon", "default" 77 | icon_set = "default", 78 | -- Set the log level for nvim-ide's log. Log can be accessed with 79 | -- 'Workspace OpenLog'. Values are 'debug', 'warn', 'info', 'error' 80 | log_level = "info", 81 | -- Component specific configurations and default config overrides. 82 | components = { 83 | -- The global keymap is applied to all Components before construction. 84 | -- It allows common keymaps such as "hide" to be overridden, without having 85 | -- to make an override entry for all Components. 86 | -- 87 | -- If a more specific keymap override is defined for a specific Component 88 | -- this takes precedence. 89 | global_keymaps = { 90 | -- example, change all Component's hide keymap to "h" 91 | -- hide = h 92 | }, 93 | -- example, prefer "x" for hide only for Explorer component. 94 | -- Explorer = { 95 | -- keymaps = { 96 | -- hide = "x", 97 | -- } 98 | -- } 99 | }, 100 | -- default panel groups to display on left and right. 101 | panels = { 102 | left = "explorer", 103 | right = "git" 104 | }, 105 | -- panels defined by groups of components, user is free to redefine the defaults 106 | -- and/or add additional. 107 | panel_groups = { 108 | explorer = { outline.Name, bufferlist.Name, explorer.Name, bookmarks.Name, callhierarchy.Name, terminalbrowser.Name }, 109 | terminal = { terminal.Name }, 110 | git = { changes.Name, commits.Name, timeline.Name, branches.Name } 111 | }, 112 | -- workspaces config 113 | workspaces = { 114 | -- which panels to open by default, one of: 'left', 'right', 'both', 'none' 115 | auto_open = 'left', 116 | }, 117 | -- default panel sizes for the different positions 118 | panel_sizes = { 119 | left = 30, 120 | right = 30, 121 | bottom = 15 122 | } 123 | }) 124 | ``` 125 | 126 | 3. Issue the "Workspace" command to begin discovering what's available. 127 | 128 | 4. Begin reading ":h nvim-ide" 129 | 130 | ## Best used with 131 | 132 | `nvim-ide` is best used with: 133 | 134 | fuzzy-searching: 135 | 136 | - [Telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) 137 | - [fzf-lua](https://github.com/ibhagwan/fzf-lua) 138 | 139 | pretty notifications: 140 | 141 | - [nvim-notify](https://github.com/rcarriga/nvim-notify) 142 | 143 | vscode-like "peek": 144 | - [glance.nvim](https://github.com/DNLHC/glance.nvim) 145 | 146 | deeper git integration: 147 | - [gitsigns](https://github.com/lewis6991/gitsigns.nvim) 148 | 149 | debugging: 150 | - [nvim-dap](https://github.com/mfussenegger/nvim-dap) 151 | - [nvim-dap-ui](https://github.com/rcarriga/nvim-dap-ui) 152 | -------------------------------------------------------------------------------- /lua/ide/lib/async_client.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | local logger = require('ide.logger.logger') 3 | 4 | local Client = {} 5 | 6 | Client.new = function(cmd) 7 | local self = { 8 | cmd = cmd, 9 | -- a default logger that's set on construction. 10 | -- a derived class can set this logger instance and base class methods will 11 | -- derive a new logger from it to handle base class component level log fields. 12 | logger = logger.new("async_client"), 13 | } 14 | 15 | -- a basic stderr reader which concats reads into req.stdout or sets 16 | -- an error if reading fails. 17 | local function _basic_stdout_read(req) 18 | return function(err, data) 19 | -- error reading stdout, capture and return. 20 | if err then 21 | req.error = true 22 | req.reason = string.format("error reading from stdout: %s", err) 23 | return 24 | end 25 | -- more data to read, concat it to stdout buffer. 26 | if data then 27 | req.stdout = req.stdout .. data 28 | return 29 | end 30 | end 31 | end 32 | 33 | -- a basic stderr reader which concats reads into req.stderr or sets 34 | -- an error if reading fails. 35 | local function _basic_stderr_read(req) 36 | return function(err, data) 37 | -- error reading stdout, capture and return. 38 | if err then 39 | req.error = true 40 | req.reason = string.format("error reading from stderr: %s", err) 41 | req.stderr_eof = true 42 | return 43 | end 44 | -- more data to read, concat it to stdout buffer. 45 | if data then 46 | req.stderr = req.stderr .. data 47 | return 48 | end 49 | end 50 | end 51 | 52 | -- helper functions to create a formatted string for logging a req. 53 | function self.log_req(req, with_output) 54 | if with_output then 55 | return string.format("\n [pid]: %s [cmd]: %s [args]: %s [error]: %s [reason]: %s [exit_code]: %s [signal]: %s \n [stderr]:\n%s \n [stdout]:\n%s" 56 | , 57 | vim.inspect(req.pid), 58 | req.cmd, 59 | req.args, 60 | vim.inspect(req.error), 61 | req.reason, 62 | vim.inspect(req.exit_code), 63 | req.signal, 64 | req.stderr, 65 | req.stdout 66 | ) 67 | end 68 | return string.format("\n [pid]: %s [cmd]: %s [args]: %s [error]: %s [reason]: %s [exit_code]: %s [signal]: %s", 69 | vim.inspect(req.pid), 70 | req.cmd, 71 | req.args, 72 | vim.inspect(req.error), 73 | req.reason, 74 | vim.inspect(req.exit_code), 75 | req.signal 76 | ) 77 | 78 | end 79 | 80 | -- makes an async request with the given arguments. 81 | -- 82 | -- @args - (@table|@string), if a table, an array of arguments. 83 | -- if a string is provided it will be split on white-space to 84 | -- resolve an array of arguments. 85 | -- @opts - @table, - options for future usage. 86 | -- @callback - @function(@table), A callback issued on request finish, 87 | -- called with a `request` table. 88 | -- request table fields: 89 | -- cmd - @string, the cli command 90 | -- args- @string, the joined arguments to the cli 91 | -- stdout - @string, the raw stdout 92 | -- stderr - @string, the raw stderr 93 | -- error - @bool, if there was an error involved in issuing the command 94 | -- reason - @string, if error is true, the reason there was an error 95 | -- exit_code - @int, the exit code of the command 96 | -- @processor - @function(@table), a callback, called with a `req` table described, 97 | -- just before the `callback` is issued. provides a hook to manipulate 98 | -- the `req` object before the callback is issued. 99 | function self.make_request(args, opts, callback, processor) 100 | local log = self.logger.logger_from(nil, "Client.make_request") 101 | 102 | if opts == nil then opts = {} end 103 | if args == nil then args = {} end 104 | -- be a nice client, and split strings for the caller. 105 | if type(args) == "string" then 106 | args = vim.fn.split(args) 107 | end 108 | 109 | local req = { 110 | cmd = self.cmd, 111 | args = vim.fn.join(args), 112 | stdout = "", 113 | stderr = "", 114 | error = false, 115 | reason = "", 116 | exit_code = nil, 117 | signal = nil, 118 | } 119 | 120 | local stdout = vim.loop.new_pipe() 121 | local stderr = vim.loop.new_pipe() 122 | local handle = nil 123 | local pid = nil 124 | 125 | local callback_wrap = function(exit_code) 126 | req.exit_code = exit_code 127 | if exit_code ~= 0 then 128 | req.error = true 129 | req.reason = "non-zero status code: " .. exit_code 130 | end 131 | req.signal = signal 132 | vim.schedule(function() 133 | log.debug("finished request, handling callbacks and closing handles %s", self.log_req(req, true)) 134 | if processor ~= nil then 135 | processor(req) 136 | end 137 | callback(req) 138 | -- close all pipes and handles only after we run our processor 139 | -- and callback to avoid close-before-read timing bugs. 140 | stdout:read_stop() 141 | stderr:read_stop() 142 | stdout:close() 143 | stderr:close() 144 | handle:close() 145 | end) 146 | end 147 | 148 | handle, pid = uv.spawn( 149 | self.cmd, 150 | { 151 | stdio = { nil, stdout, stderr }, 152 | args = args, 153 | verbatim = true 154 | }, 155 | callback_wrap 156 | ) 157 | req.pid = pid 158 | log.debug("made request %s", self.log_req(req)) 159 | 160 | stdout:read_start(_basic_stdout_read(req)) 161 | stderr:read_start(_basic_stderr_read(req)) 162 | end 163 | 164 | function self.make_json_request(args, opts, callback) 165 | self.make_request(args, opts, callback, function(req) 166 | local ok, out = pcall(vim.fn.json_decode, req.stdout) 167 | if not ok then 168 | req.error = true 169 | req.reason = string.format("error decoding json: %s", out) 170 | return 171 | end 172 | req.stdout = out 173 | end) 174 | end 175 | 176 | function self.make_nl_request(args, opts, callback) 177 | self.make_request(args, opts, callback, function(req) 178 | local ok, out = pcall(vim.fn.split, req.stdout, "\n") 179 | if not ok then 180 | req.error = true 181 | req.reason = string.format("error splitting text by new lines: %s", out) 182 | return 183 | end 184 | req.stdout = out 185 | end) 186 | end 187 | 188 | return self 189 | end 190 | 191 | return Client 192 | -------------------------------------------------------------------------------- /lua/ide/buffers/diffbuffer.lua: -------------------------------------------------------------------------------- 1 | local libwin = require("ide.lib.win") 2 | local libbuf = require("ide.lib.buf") 3 | local buffer = require("ide.buffers.buffer") 4 | local workspace_registry = require("ide.workspaces.workspace_registry") 5 | 6 | local DiffBuffer = {} 7 | 8 | -- DiffBuffer is actually an abstraction over two buffers and associated windows. 9 | -- 10 | -- A DiffBuffer will reliably create two vsplit windows, provide an API for placing 11 | -- content into both, and perform a `vimdiff` over the contents. 12 | DiffBuffer.new = function(path_a, path_b) 13 | self = { 14 | path_a = path_a, 15 | path_b = path_b, 16 | buffer_a = nil, 17 | buffer_b = nil, 18 | win_a = nil, 19 | win_b = nil, 20 | } 21 | 22 | -- Setup a new DiffBuffer view. 23 | -- 24 | -- After this function is ran both win_a and win_b will have been created, 25 | -- displaying their associated buffer_a and buff_b, respectively. 26 | -- 27 | -- All other windows other then component windows will have been closed. 28 | -- 29 | -- The caller can decide to perform this action in a new tab if they would 30 | -- rather not disrupt the current one. 31 | -- 32 | -- No vimdiff commands are issued as part of this function, the caller should 33 | -- continue to load the contents of buffer_a and buffer_b and then use the 34 | -- `diff` function to configure these buffers as a vimdiff. 35 | function self.setup() 36 | -- find any non-component windows in the current tab. 37 | local wins = {} 38 | for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do 39 | if not libwin.is_component_win(w) then 40 | table.insert(wins, w) 41 | end 42 | end 43 | 44 | -- if no wins, create one 45 | if #wins == 0 then 46 | vim.cmd("vsplit") 47 | -- may inherit the SB win highlight, reset it. 48 | vim.api.nvim_win_set_option(cur_win, "winhighlight", "Normal:Normal") 49 | else 50 | vim.api.nvim_set_current_win(wins[1]) 51 | end 52 | 53 | -- close all other wins 54 | for _, w in ipairs(wins) do 55 | if w ~= wins[1] and libwin.win_is_valid(w) then 56 | vim.api.nvim_win_close(w, true) 57 | end 58 | end 59 | 60 | self.win_a = vim.api.nvim_get_current_win() 61 | -- vertical split to get win_b 62 | vim.cmd("vsplit") 63 | 64 | self.win_b = vim.api.nvim_get_current_win() 65 | end 66 | 67 | -- Write a series of lines into either buffer_a or buffer_b. 68 | -- 69 | -- The chosen buffer will be truncated first and the lines are not appended 70 | -- to the buffer. 71 | -- 72 | -- Thus, the intended use of this method is to write all lines to be diff'd 73 | -- at once into the buffer_a or buffer_b. 74 | -- 75 | -- @lines - @table, an array of strings to write to the buffer. 76 | -- @which - @string, a string of either "a" or "b" indicating which diff 77 | -- buffer to write the lines to. 78 | function self.write_lines(lines, which, opts) 79 | local o = { 80 | listed = true, 81 | scratch = false, 82 | modifiable = true, 83 | } 84 | o = vim.tbl_extend("force", o, opts) 85 | 86 | local buf = nil 87 | if which == "a" then 88 | if self.buffer_a == nil then 89 | self.buffer_a = buffer.new(nil, o.listed, o.scratch) 90 | vim.api.nvim_win_set_buf(self.win_a, self.buffer_a.buf) 91 | end 92 | buf = self.buffer_a 93 | elseif which == "b" then 94 | if self.buffer_b == nil then 95 | self.buffer_b = buffer.new(nil, o.listed, o.scratch) 96 | vim.api.nvim_win_set_buf(self.win_b, self.buffer_b.buf) 97 | end 98 | buf = self.buffer_b 99 | end 100 | -- set buftype to nofile since we have no file backing it 101 | vim.api.nvim_buf_set_option(buf.buf, "buftype", "nofile") 102 | 103 | buf.set_modifiable(o.modifiable) 104 | 105 | buf.truncate() 106 | buf.write_lines(lines) 107 | end 108 | 109 | -- Open a particular file system path in either diff window a or b. 110 | -- 111 | -- The buffer_a or buffer_b fields of @DiffBuffer will be updated with the 112 | -- new buffer that was opened. 113 | function self.open_buffer(path, which, opts) 114 | local win = nil 115 | if which == "a" then 116 | win = self.win_a 117 | libwin.open_buffer(win, path) 118 | self.buffer_a = buffer.new(vim.api.nvim_win_get_buf(0)) 119 | self.path_a = path 120 | elseif which == "b" then 121 | win = self.win_b 122 | libwin.open_buffer(win, path) 123 | self.buffer_b = buffer.new(vim.api.nvim_win_get_buf(0)) 124 | self.path_b = path 125 | end 126 | end 127 | 128 | -- Perform a vimdiff over win_a and win_b in their current configuration. 129 | -- 130 | -- The caller should have either written content to buffer_a and buffer_b, or 131 | -- opened a file in win_a or win_b, or some combination of both, before 132 | -- calling this. 133 | function self.diff() 134 | if not libwin.win_is_valid(self.win_a) or not libwin.win_is_valid(self.win_b) then 135 | return 136 | end 137 | vim.api.nvim_set_current_win(self.win_a) 138 | vim.cmd("filetype detect") 139 | vim.cmd("diffthis") 140 | vim.api.nvim_set_current_win(self.win_b) 141 | vim.cmd("filetype detect") 142 | vim.cmd("diffthis") 143 | 144 | -- setup autcommands which rip the entire diff buffer down on :q of either 145 | -- a or b 146 | local aucmd_a = nil 147 | local aucmd_b = nil 148 | 149 | function create_win_if_last(buf) 150 | if not libbuf.buf_is_valid(buf) then 151 | vim.cmd("vsplit") 152 | end 153 | end 154 | 155 | aucmd_a = vim.api.nvim_create_autocmd("QuitPre", { 156 | callback = function(args) 157 | local win = vim.api.nvim_get_current_win() 158 | if libwin.win_is_valid(win) and win == self.win_a then 159 | -- win_a is going to close so lets recover from diff in win_a 160 | local buf_to_use = libbuf.next_regular_buffer() 161 | if buf_to_use == nil then 162 | buf_to_use = vim.api.nvim_create_buf(false, false) 163 | end 164 | if libwin.win_is_valid(self.win_b) then 165 | vim.api.nvim_win_set_buf(self.win_b, buf_to_use) 166 | -- weird but sometimes we loose the filetype when switching back 167 | vim.cmd("filetype detect") 168 | end 169 | 170 | if libbuf.buf_is_valid(self.buffer_a.buf) then 171 | vim.api.nvim_buf_delete(self.buffer_a.buf, { force = true }) 172 | end 173 | if libbuf.buf_is_valid(self.buffer_b.buf) then 174 | vim.api.nvim_buf_delete(self.buffer_b.buf, { force = true }) 175 | end 176 | 177 | create_win_if_last(buf_to_use) 178 | vim.api.nvim_del_autocmd(aucmd_a) 179 | vim.api.nvim_del_autocmd(aucmd_b) 180 | end 181 | end, 182 | }) 183 | 184 | aucmd_b = vim.api.nvim_create_autocmd("QuitPre", { 185 | callback = function(args) 186 | local win = vim.api.nvim_get_current_win() 187 | if libwin.win_is_valid(win) and win == self.win_b then 188 | -- win_b is going to close so lets recover from diff in win_a 189 | local buf_to_use = libbuf.next_regular_buffer() 190 | if buf_to_use == nil then 191 | buf_to_use = vim.api.nvim_create_buf(false, false) 192 | end 193 | if libwin.win_is_valid(self.win_a) then 194 | vim.api.nvim_win_set_buf(self.win_a, buf_to_use) 195 | -- weird but sometimes we loose the filetype when switching back 196 | vim.cmd("filetype detect") 197 | end 198 | 199 | create_win_if_last(buf_to_use) 200 | 201 | if libbuf.buf_is_valid(self.buffer_a.buf) then 202 | vim.api.nvim_buf_delete(self.buffer_a.buf, { force = true }) 203 | end 204 | if libbuf.buf_is_valid(self.buffer_b.buf) then 205 | vim.api.nvim_buf_delete(self.buffer_b.buf, { force = true }) 206 | end 207 | 208 | create_win_if_last(buf_to_use) 209 | vim.api.nvim_del_autocmd(aucmd_a) 210 | vim.api.nvim_del_autocmd(aucmd_b) 211 | end 212 | end, 213 | }) 214 | end 215 | 216 | return self 217 | end 218 | 219 | return DiffBuffer 220 | -------------------------------------------------------------------------------- /lua/ide/components/bufferlist/component.lua: -------------------------------------------------------------------------------- 1 | local base = require("ide.panels.component") 2 | local sort = require("ide.lib.sort") 3 | local libwin = require("ide.lib.win") 4 | local libbuf = require("ide.lib.buf") 5 | local libws = require("ide.lib.workspace") 6 | local logger = require("ide.logger.logger") 7 | local icons = require("ide.icons") 8 | local commands = require("ide.components.bufferlist.commands") 9 | 10 | local BufferListComponent = {} 11 | 12 | local config_prototype = { 13 | default_height = nil, 14 | -- float the current buffer to the top of list 15 | current_buffer_top = false, 16 | -- disable all keymaps 17 | disabled_keymaps = false, 18 | hidden = false, 19 | keymaps = { 20 | edit = "", 21 | edit_split = "s", 22 | edit_vsplit = "v", 23 | delete = "d", 24 | hide = "H", 25 | close = "X", 26 | details = "d", 27 | help = "?" 28 | }, 29 | } 30 | 31 | BufferListComponent.new = function(name, config) 32 | local self = base.new(name) 33 | self.bufs = {} 34 | self.logger = logger.new("bufferlist") 35 | self.buf = nil 36 | self.augroup = vim.api.nvim_create_augroup("NvimIdeBufferlist", { clear = true }) 37 | 38 | self.config = vim.deepcopy(config_prototype) 39 | if config ~= nil then 40 | self.config = vim.tbl_deep_extend("force", config_prototype, config) 41 | end 42 | 43 | self.hidden = self.config.hidden 44 | 45 | local function setup_buffer() 46 | local log = self.logger.logger_from(nil, "Component._setup_buffer") 47 | 48 | local buf = vim.api.nvim_create_buf(false, true) 49 | vim.api.nvim_buf_set_option(buf, "bufhidden", "hide") 50 | vim.api.nvim_buf_set_option(buf, "filetype", "bufferlist") 51 | vim.api.nvim_buf_set_option(buf, "buftype", "nofile") 52 | vim.api.nvim_buf_set_option(buf, "modifiable", false) 53 | vim.api.nvim_buf_set_option(buf, "swapfile", false) 54 | vim.api.nvim_buf_set_option(buf, "textwidth", 0) 55 | vim.api.nvim_buf_set_option(buf, "wrapmargin", 0) 56 | 57 | local keymaps = { 58 | { 59 | key = self.config.keymaps.edit, 60 | cb = function() 61 | self.open_buf({ fargs = {} }) 62 | end, 63 | }, 64 | { 65 | key = self.config.keymaps.edit_split, 66 | cb = function() 67 | self.open_buf({ fargs = { "split" } }) 68 | end, 69 | }, 70 | { 71 | key = self.config.keymaps.edit_vsplit, 72 | cb = function() 73 | self.open_buf({ fargs = { "vsplit" } }) 74 | end, 75 | }, 76 | { 77 | key = self.config.keymaps.delete, 78 | cb = function() 79 | self.close_buf() 80 | end, 81 | }, 82 | { 83 | key = self.config.keymaps.hide, 84 | cb = function() 85 | self.hide() 86 | end, 87 | }, 88 | { 89 | key = self.config.keymaps.help, 90 | cb = function() 91 | self.help_keymaps() 92 | end, 93 | }, 94 | } 95 | 96 | if not self.config.disable_keymaps then 97 | for _, keymap in ipairs(keymaps) do 98 | print(vim.inspect(keymap)) 99 | libbuf.set_keymap_normal(buf, keymap.key, keymap.cb) 100 | end 101 | end 102 | 103 | -- setup autocmds to refresh 104 | vim.api.nvim_create_autocmd({ "BufAdd", "BufDelete", "BufWipeout" }, { 105 | callback = function() 106 | vim.schedule(self.refresh) 107 | end, 108 | group = self.augroup, 109 | }) 110 | vim.api.nvim_create_autocmd({ "CursorHold" }, { 111 | callback = function(args) 112 | if not libws.is_current_ws(self.workspace) then 113 | return 114 | end 115 | if libbuf.is_listed_buf(args.buf) then 116 | vim.schedule(self.refresh) 117 | end 118 | end, 119 | group = self.augroup, 120 | }) 121 | 122 | return buf 123 | end 124 | 125 | function self.open() 126 | local log = self.logger.logger_from(nil, "Component.open") 127 | log.debug("BufferList component opening, workspace %s", vim.api.nvim_get_current_tabpage()) 128 | 129 | -- init buf if not already 130 | if self.buf == nil then 131 | log.debug("buffer does not exist, creating.", vim.api.nvim_get_current_tabpage()) 132 | self.buf = setup_buffer() 133 | end 134 | 135 | log.debug("using buffer %d", self.buf) 136 | 137 | -- initial render 138 | self.refresh() 139 | return self.buf 140 | end 141 | 142 | function self.post_win_create() 143 | local log = self.logger.logger_from(nil, "Component.post_win_create") 144 | -- setup web-dev-icons highlights if available 145 | if pcall(require, "nvim-web-devicons") then 146 | for _, icon_data in pairs(require("nvim-web-devicons").get_icons()) do 147 | local hl = "DevIcon" .. icon_data.name 148 | vim.cmd(string.format("syn match %s /%s/", hl, icon_data.icon)) 149 | end 150 | end 151 | -- set highlights for global icon theme 152 | icons.global_icon_set.set_win_highlights() 153 | libwin.set_winbar_title(0, "BUFFERS") 154 | end 155 | 156 | function self.get_commands() 157 | return commands.new(self).get() 158 | end 159 | 160 | function self.refresh() 161 | local cur_buf = vim.api.nvim_get_current_buf() 162 | local listed_bufs = libbuf.get_listed_bufs() 163 | if #listed_bufs == 0 then 164 | return 165 | end 166 | 167 | local bufs = vim.tbl_map(function(buf) 168 | local icon 169 | -- use webdev icons if possible 170 | if pcall(require, "nvim-web-devicons") then 171 | local filename = vim.api.nvim_buf_get_name(buf) 172 | local ext = vim.fn.fnamemodify(filename, ":e:e") 173 | icon = require("nvim-web-devicons").get_icon(filename, ext, { default = true }) 174 | if self.kind == "dir" then 175 | icon = require("nvim-web-devicons").get_icon("dir") 176 | end 177 | else 178 | if self.kind ~= "dir" then 179 | icon = icons.global_icon_set.get_icon("File") 180 | else 181 | icon = icons.global_icon_set.get_icon("Folder") 182 | end 183 | end 184 | return { 185 | name = libbuf.get_unique_filename(vim.api.nvim_buf_get_name(buf)), 186 | icon = icon, 187 | id = buf, 188 | is_current = (cur_buf == buf), 189 | } 190 | end, libbuf.get_listed_bufs()) 191 | self.bufs = bufs 192 | if self.config.current_buffer_top then 193 | sort(self.bufs, function(a, _) 194 | if a.is_current then 195 | return true 196 | end 197 | return false 198 | end) 199 | end 200 | self.render() 201 | end 202 | 203 | function self.buf_under_cursor() 204 | return self.bufs[self.state["cursor"].cursor[1]] 205 | end 206 | 207 | function self.render() 208 | local lines = {} 209 | for i, buf in ipairs(self.bufs) do 210 | local line = string.format(" %s %s ", buf.icon, buf.name) 211 | if buf.is_current then 212 | line = string.format(" %s * %s ", buf.icon, buf.name) 213 | -- track the currently opened buffer if we are displayed. 214 | if self.is_displayed() then 215 | libwin.safe_cursor_restore(self.win, { i, 1 }) 216 | end 217 | end 218 | if vim.api.nvim_buf_get_option(buf.id, "modified") then 219 | line = line .. " [+]" 220 | end 221 | table.insert(lines, line) 222 | end 223 | 224 | vim.api.nvim_buf_set_option(self.buf, "modifiable", true) 225 | vim.api.nvim_buf_set_lines(self.buf, 0, -1, false, lines) 226 | vim.api.nvim_buf_set_option(self.buf, "modifiable", false) 227 | end 228 | 229 | function self.open_buf(args) 230 | local log = self.logger.logger_from(nil, "Component.open_buf") 231 | 232 | local split_type = vim.tbl_get(args or {}, "fargs", 1) 233 | local bufnode = self.buf_under_cursor() 234 | if not bufnode then 235 | log.error("component failed to unmarshal buffer from list") 236 | return 237 | end 238 | 239 | if self.workspace == nil then 240 | log.error("component has a nil workspace, can't open filenode %s", fnode.path) 241 | end 242 | 243 | local win = self.workspace.get_win() 244 | vim.api.nvim_set_current_win(win) 245 | 246 | if split_type == "split" then 247 | vim.cmd("split") 248 | elseif split_type == "vsplit" then 249 | vim.cmd("vsplit") 250 | end 251 | 252 | vim.api.nvim_win_set_buf(win, bufnode.id) 253 | end 254 | 255 | function self.close_buf() 256 | local log = self.logger.logger_from(nil, "Component.open_buf") 257 | 258 | local bufnode = self.buf_under_cursor() 259 | if not bufnode then 260 | log.error("component failed to unmarshal buffer from list") 261 | return 262 | end 263 | 264 | vim.api.nvim_buf_delete(bufnode.id, {}) 265 | self.refresh() 266 | end 267 | 268 | return self 269 | end 270 | 271 | return BufferListComponent 272 | -------------------------------------------------------------------------------- /lua/ide/trees/tree.lua: -------------------------------------------------------------------------------- 1 | local logger = require("ide.logger.logger") 2 | local marshaller = require("ide.trees.marshaller") 3 | local depth_table = require("ide.trees.depth_table") 4 | 5 | local Tree = {} 6 | 7 | Tree.new = function(type) 8 | local self = { 9 | -- The root @Node of the tree 10 | root = nil, 11 | -- An optional type of the tree. 12 | type = "", 13 | -- A table associating the tree's depth (0-n) with the list of @Node(s) at 14 | -- that depth. 15 | -- 16 | -- useful for linear searches at a specific depth of the tree. 17 | depth_table = depth_table.new(), 18 | -- A buffer directly associated with the tree. 19 | -- 20 | -- If a buffer is associated with a tree, the tree can be marshalled into 21 | -- lines and written to it. 22 | buffer = nil, 23 | -- A @Marshaller implementation used to marshal the @Tree into @Tree.buffer. 24 | marshaller = marshaller.new(), 25 | } 26 | if type ~= nil then 27 | self.type = type 28 | end 29 | 30 | -- Sets the Tree's buffer in which it will marshal itself into. 31 | -- 32 | -- @buf - buffer id, a buffer in which the current tree's nodes will be 33 | -- marshalled into, providind a UI for the tree. 34 | -- 35 | -- return: void 36 | function self.set_buffer(buf) 37 | self.buffer = buf 38 | end 39 | 40 | -- Return a buffer is one is set with set_buffer. 41 | -- 42 | -- return: buffer id | nil, a buffer in which the current tree's nodes will be 43 | -- marshalled into, providind a UI for the tree. 44 | function self.get_buffer() 45 | return self.buffer 46 | end 47 | 48 | local function _node_search(Node) 49 | local found = nil 50 | if Node.depth ~= nil then 51 | -- try a quick search of dpt tree 52 | found = self.depth_table.search(Node.depth, Node.key) 53 | end 54 | if found == nil then 55 | -- try a slow search via walking 56 | found = self.search_key(Node.key) 57 | end 58 | return found 59 | end 60 | 61 | -- Add the provided @children to the tree as children of @parent, unless 62 | -- @external is true. 63 | -- 64 | -- @parent - A @Node at which @children will be attached linked to. 65 | -- if @parent.depth is set to 0 this indicates to @Tree that the 66 | -- caller is building a new tree and will discard the previous, if 67 | -- exists. 68 | -- @children - An array of @Node which will be linked to @parent as its 69 | -- children. 70 | -- If @parent.depth is not 0, a search for @parent will be 71 | -- performed, and thus its expected @parent is an existing 72 | -- @Node in the @Tree. 73 | -- When constructing the @Node(s) in this array their depth's 74 | -- field should be left blank (if external is not used). 75 | -- Their depths will be computed when added to the parent. 76 | -- @opts - A table of options which instruct add_node how to proceed. 77 | -- Fields: 78 | -- external - A @bool. If set to true this indicates the tree's @Node 79 | -- hierarchy has been built by the caller and this method 80 | -- will remove the current @Tree.root and replace it with 81 | -- @parent. The @children field is ignored. 82 | -- 83 | -- append - If true Append @children to the existing set of @parent's 84 | -- children. If false, remove the current children in favor 85 | -- of the provided @children array. 86 | function self.add_node(parent, children, opts) 87 | if opts == nil then 88 | opts = { 89 | external = false, 90 | append = false, 91 | } 92 | end 93 | 94 | -- external nodes are roots of trees built externally 95 | -- if this is true set the tree's root to the incoming parent 96 | -- and immediately return 97 | if opts["external"] then 98 | self.root = parent 99 | self.depth_table.refresh(self.root) 100 | return 101 | end 102 | 103 | -- if depth is 0 we are creating a new call tree, discard the old one 104 | -- by overwriting the current root, populate depth_table so fast lookup 105 | -- makes the double work here negligible. 106 | if parent.depth == 0 then 107 | self.root = parent 108 | parent.tree = self 109 | self.depth_table.refresh(self.root) 110 | end 111 | 112 | local pNode = _node_search(parent) 113 | 114 | if pNode == nil then 115 | error("could not find parent node in tree. key: " .. parent.key) 116 | end 117 | 118 | if not opts["append"] then 119 | pNode.children = (function() 120 | return {} 121 | end)() 122 | end 123 | 124 | local child_depth = pNode.depth + 1 125 | for _, c in ipairs(children) do 126 | c.depth = child_depth 127 | c.parent = parent 128 | c.tree = self 129 | table.insert(pNode.children, c) 130 | end 131 | self.depth_table.refresh(self.root) 132 | end 133 | 134 | function self.remove_subtree(root) 135 | local pNode = _node_search(root) 136 | if pNode == nil then 137 | return 138 | end 139 | -- remove node's children 140 | pNode.children = (function() 141 | return {} 142 | end)() 143 | self.depth_table.refresh(self.root) 144 | end 145 | 146 | -- Walks the subtree from @root and performs @action 147 | -- 148 | -- @root - @Node, a root @Node to start the @Tree walk. 149 | -- 150 | -- @action - @function(@Node), a function which is called at very node, 151 | -- including @root. 152 | -- @Node is the current node in the walk and can be manipulated 153 | -- within the @action function. 154 | -- The action function must return a @bool which if false, ends 155 | -- the walk. 156 | function self.walk_subtree(root, action) 157 | local pNode = _node_search(root) 158 | if pNode == nil then 159 | return 160 | end 161 | if not action(pNode) then 162 | return 163 | end 164 | for _, c in ipairs(pNode.children) do 165 | self.walk_subtree(c, action) 166 | end 167 | end 168 | 169 | -- Search for a @Node in the @Tree by its key. 170 | -- 171 | -- @key - string, a unique key for a @Node in the tree. 172 | -- 173 | -- return: @Node | nil 174 | function self.search_key(key) 175 | local found = nil 176 | self.walk_subtree(self.root, function(node) 177 | if node.key == key then 178 | found = node 179 | return false 180 | end 181 | return true 182 | end) 183 | return found 184 | end 185 | 186 | -- Expands the provided @Node 187 | -- 188 | -- @Node - @Node, a node to expand in the @Tree 189 | function self.expand_node(Node) 190 | local pNode = _node_search(Node) 191 | if pNode == nil then 192 | return 193 | end 194 | -- the node can optionally perform an action on expanding for dynamic 195 | -- population of children. 196 | if pNode.expand ~= nil then 197 | pNode.expand() 198 | -- the implemented expand function may not have set the node's parent, 199 | -- (tho it should), be defensive and do it anyway. 200 | for _, c in ipairs(pNode.children) do 201 | c.parent = pNode 202 | end 203 | end 204 | pNode.expanded = true 205 | end 206 | 207 | -- Collapses the provided @Node 208 | -- 209 | -- If any children nodes are expanded they will continue to be once @Node 210 | -- is expanded again. 211 | -- 212 | -- @Node - @Node, a node to collapse in the @Tree 213 | function self.collapse_node(Node) 214 | local pNode = _node_search(Node) 215 | if pNode == nil then 216 | return 217 | end 218 | pNode.expanded = false 219 | end 220 | 221 | -- Collapses all @Node(s) from the @root down. 222 | -- 223 | -- @root - @Node, the root node to collapse. All children will be collapsed 224 | -- from this root down. 225 | function self.collapse_subtree(root) 226 | self.walk_subtree(root, function(Node) 227 | Node.expanded = false 228 | return true 229 | end) 230 | end 231 | 232 | local function recursive_reparent(Node, depth) 233 | local pNode = _node_search(Node) 234 | if pNode == nil then 235 | return 236 | end 237 | -- we are the new root, dump the current root_node and 238 | -- set yourself 239 | if depth == 0 then 240 | self.root = pNode 241 | self.root.depth = 0 242 | end 243 | -- recurse to leafs 244 | for _, child in ipairs(pNode.children) do 245 | recursive_reparent(child, depth + 1) 246 | -- recursion done, update your depth 247 | child.depth = depth + 1 248 | end 249 | if depth == 0 then 250 | self.depth_table.refresh(self.root) 251 | end 252 | end 253 | 254 | -- Make @Node the new root of the @Tree 255 | -- 256 | -- @Node - the @Node to make the new root of the @Tree. 257 | function self.reparent_node(Node) 258 | recursive_reparent(Node, 0) 259 | end 260 | 261 | function self.marshal(opts) 262 | if self.buffer == nil or (not vim.api.nvim_buf_is_valid(self.buffer)) then 263 | return 264 | end 265 | self.marshaller.marshal(self, opts) 266 | end 267 | 268 | -- When a tree has been marshalled into an associated buffer this method 269 | -- can return the @Node associated with the marshalled buffer line. 270 | -- 271 | -- @linenr - integer, line number within @Tree.buffer. 272 | -- 273 | -- return: @Node | nil, the node which associates with the marshalled buffer 274 | -- line. 275 | function self.unmarshal(linenr) 276 | if linenr == nil then 277 | error("cannot call unmarshal with a nil linenr") 278 | end 279 | local node = self.marshaller.unmarshal(linenr) 280 | return node 281 | end 282 | 283 | return self 284 | end 285 | 286 | return Tree 287 | -------------------------------------------------------------------------------- /lua/ide/components/bookmarks/notebook.lua: -------------------------------------------------------------------------------- 1 | local tree = require("ide.trees.tree") 2 | local libbuf = require("ide.lib.buf") 3 | local libws = require("ide.lib.workspace") 4 | local icons = require("ide.icons") 5 | local bookmarknode = require("ide.components.bookmarks.bookmarknode") 6 | local base64 = require("ide.lib.encoding.base64") 7 | 8 | local Notebook = {} 9 | 10 | Notebook.RECORD_SEP = "␞" 11 | Notebook.GROUP_SEP = "␝" 12 | 13 | local bookmarks_ns = vim.api.nvim_create_namespace("bookmarks-ns") 14 | 15 | Notebook.new = function(buf, name, file, bookmarks_component) 16 | local self = { 17 | name = name, 18 | buf = buf, 19 | file = file, 20 | tree = tree.new("bookmarks"), 21 | bookmarks = nil, 22 | sync_aucmd = nil, 23 | write_aucmd = nil, 24 | component = bookmarks_component, 25 | tracking = {}, 26 | } 27 | self.tree.set_buffer(buf) 28 | 29 | if vim.fn.glob(file) == "" then 30 | error("attempted to open notebook for non-existent notebook directory") 31 | end 32 | 33 | local function _get_bookmark_file_full(buf_name) 34 | local bookmark_file = base64.encode(buf_name) 35 | local bookmark_file_path = self.file .. "/" .. bookmark_file 36 | local full_path = vim.fn.fnamemodify(bookmark_file_path, ":p") 37 | return full_path 38 | end 39 | 40 | function _remove_extmark(node) 41 | if self.tracking[node.file] == nil then 42 | return 43 | end 44 | local tracked = {} 45 | for _, bm in ipairs(self.tracking[node.file]) do 46 | if bm.key == node.key then 47 | vim.api.nvim_buf_del_extmark(bm.mark[1], bm.mark[3], bm.mark[2]) 48 | goto continue 49 | end 50 | table.insert(tracked, bm) 51 | ::continue:: 52 | end 53 | self.tracking[node.file] = (function() 54 | return {} 55 | end)() 56 | self.tracking[node.file] = tracked 57 | end 58 | 59 | local function _create_extmark(buf, bm) 60 | local mark = vim.api.nvim_buf_set_extmark(buf, bookmarks_ns, bm.start_line - 1, 0, { 61 | virt_text_pos = "right_align", 62 | virt_text = { { icons.global_icon_set.get_icon("Bookmark") .. " " .. bm.title, "Keyword" } }, 63 | hl_mode = "combine", 64 | }) 65 | bm.mark = { buf, mark, bookmarks_ns } 66 | if self.tracking[bm.file] == nil then 67 | self.tracking[bm.file] = {} 68 | end 69 | table.insert(self.tracking[bm.file], bm) 70 | end 71 | 72 | -- Loads bookmarks from a notebook directory. 73 | -- 74 | -- A notebook directory organizes bookmarks in a per-buffer fashion. 75 | -- Thus, the notebook directory is read and each file is a particular 76 | -- buffer's bookmarks. 77 | -- 78 | -- Each bookmark within a per-buffer bookmark file is then marshalled into 79 | -- a @BookmarkNode 80 | function self.load_bookmarks() 81 | local nodes = {} 82 | for _, bookmark_file in ipairs(vim.fn.readdir(self.file)) do 83 | local bookmark_path = self.file .. "/" .. bookmark_file 84 | local lines = vim.fn.readfile(bookmark_path) 85 | for _, l in ipairs(lines) do 86 | local bm = bookmarknode.unmarshal_text(l) 87 | table.insert(nodes, bm) 88 | end 89 | end 90 | local root = bookmarknode.new("root", 0, 0, name, "", 0) 91 | self.tree.add_node(root, nodes) 92 | self.tree.marshal({ no_guides = true }) 93 | end 94 | 95 | -- Do an initial loading of bookmarks on creation. 96 | self.load_bookmarks() 97 | 98 | -- append a bookmark to the notebook file, this is done on create so the 99 | -- use doesn't need to save the first bookmark they create. 100 | function self.append_bookmark_file(buf_name, bm) 101 | buf_name = vim.fn.fnamemodify(buf_name, ":.") 102 | local l = bm.marshal_text() 103 | local bookmark_file = _get_bookmark_file_full(buf_name) 104 | vim.fn.writefile({ l }, bookmark_file, "a") 105 | bm.dirty = false 106 | end 107 | 108 | -- removes a bookmark in the bookmark file associated with buf_name by 109 | -- reading the file in, excluding the line at index 'i' and writing it back 110 | -- to disk. 111 | function self.remove_bookmark_file(buf_name, i) 112 | buf_name = vim.fn.fnamemodify(buf_name, ":.") 113 | local bookmark_file = _get_bookmark_file_full(buf_name) 114 | local lines = vim.fn.readfile(bookmark_file) 115 | 116 | if #lines < i then 117 | return 118 | end 119 | 120 | local new_lines = {} 121 | for ii, l in ipairs(lines) do 122 | if i ~= ii then 123 | table.insert(new_lines, l) 124 | end 125 | end 126 | 127 | vim.fn.writefile(new_lines, bookmark_file) 128 | end 129 | 130 | function self.create_bookmark() 131 | local buf = vim.api.nvim_get_current_buf() 132 | if not libbuf.is_regular_buffer(buf) then 133 | vim.notify("Can only create bookmarks on source code buffers.", vim.log.levels.Error, { 134 | title = "Bookmarks", 135 | }) 136 | return 137 | end 138 | local file = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ":.") 139 | local cursor = vim.api.nvim_win_get_cursor(0) 140 | vim.ui.input({ 141 | prompt = "Give bookmark a title: ", 142 | }, function(input) 143 | if input == nil or input == "" then 144 | return 145 | end 146 | local bm = bookmarknode.new(file, cursor[1], cursor[1], input) 147 | self.append_bookmark_file(vim.api.nvim_buf_get_name(buf), bm) 148 | bm.original_start_line = cursor[1] 149 | self.tree.add_node(self.tree.root, { bm }, { append = true }) 150 | self.tree.marshal({ no_guides = true }) 151 | _create_extmark(buf, bm) 152 | end) 153 | end 154 | 155 | -- Removes a bookmark from memory and, if written, from disk. 156 | function self.remove_bookmark(key) 157 | local new_children = {} 158 | local node = nil 159 | local index = nil 160 | for i, n in ipairs(self.tree.depth_table.table[1]) do 161 | if n.key ~= key then 162 | table.insert(new_children, n) 163 | else 164 | index = i 165 | node = n 166 | end 167 | end 168 | 169 | -- remove from disk 170 | self.remove_bookmark_file(node.file, index) 171 | 172 | -- remove from memory 173 | self.tree.root.children = (function() 174 | return {} 175 | end)() 176 | self.tree.add_node(self.tree.root, new_children) 177 | self.tree.marshal({ no_guides = true }) 178 | _remove_extmark(node) 179 | end 180 | 181 | function self.close() 182 | if self.sync_aucmd ~= nil then 183 | vim.api.nvim_del_autocmd(self.sync_aucmd) 184 | end 185 | end 186 | 187 | -- Writes the bookmarks for a given buffer name (relative to root), to 188 | -- disk. 189 | -- 190 | -- Bookmarks are organized by buffer such that saving a buffer persists any 191 | -- bookmarks created for the buffer. 192 | -- 193 | -- This allows for bookmarks to move around and be tracked in-memory but 194 | -- their ultimate position only persisted to disk when the buffer is also 195 | -- written. 196 | function self.write_bookmarks(buf_name) 197 | local lines = {} 198 | buf_name = vim.fn.fnamemodify(buf_name, ":.") 199 | self.tree.walk_subtree(self.tree.root, function(bm) 200 | if bm.file == buf_name then 201 | local l = bm.marshal_text() 202 | table.insert(lines, l) 203 | end 204 | -- set dirty to false, it will be written to disk next. 205 | bm.dirty = false 206 | bm.original_start_line = bm.start_line 207 | return true 208 | end) 209 | local bookmark_file = _get_bookmark_file_full(buf_name) 210 | vim.fn.writefile({}, bookmark_file) 211 | vim.fn.writefile(lines, bookmark_file) 212 | self.tree.marshal({ no_guides = true }) 213 | end 214 | 215 | -- For the given buffer name, update any in-memory bookmark positions and dirty 216 | -- field if they have moved. 217 | function _sync_moved_bookmarks(buf_name) 218 | if self.tracking[buf_name] == nil then 219 | return 220 | end 221 | 222 | for _, bm in ipairs(self.tracking[buf_name]) do 223 | local mark = vim.api.nvim_buf_get_extmark_by_id(bm.mark[1], bm.mark[3], bm.mark[2], {}) 224 | if bm.original_start_line ~= nil then 225 | if (mark[1] + 1) ~= bm.original_start_line then 226 | bm.dirty = true 227 | else 228 | bm.dirty = false 229 | end 230 | end 231 | bm.start_line = mark[1] + 1 232 | end 233 | end 234 | 235 | function _sync_removed_bookmarks(buf_name) 236 | if self.tracking[buf_name] == nil then 237 | return 238 | end 239 | 240 | local tracked = {} 241 | for _, bm in ipairs(self.tracking[buf_name]) do 242 | local present = self.tree.search_key(bm.key) 243 | if present == nil then 244 | vim.api.nvim_buf_del_extmark(bm.mark[1], bm.mark[3], bm.mark[2]) 245 | goto continue 246 | end 247 | table.insert(tracked, bm) 248 | ::continue:: 249 | end 250 | 251 | self.tracking[buf_name] = (function() 252 | return {} 253 | end)() 254 | self.tracking[buf_name] = tracked 255 | end 256 | 257 | function _sync_missing_bookmarks(buf, buf_name) 258 | if self.tracking[buf_name] == nil then 259 | self.tracking[buf_name] = {} 260 | end 261 | for _, bm in ipairs(self.tree.depth_table.table[1]) do 262 | if bm.file == buf_name and bm.mark == nil then 263 | _create_extmark(buf, bm) 264 | end 265 | end 266 | end 267 | 268 | -- internal function for performing a sync between in-memory bookmarks 269 | -- and an open buffer. 270 | function _buf_sync_bookmarks(buf, buf_name) 271 | if not libbuf.is_regular_buffer(buf) then 272 | return 273 | end 274 | 275 | if self.tree.depth_table.table[1] == nil then 276 | return 277 | end 278 | 279 | buf_name = vim.fn.fnamemodify(buf_name, ":.") 280 | 281 | _sync_moved_bookmarks(buf_name) 282 | 283 | _sync_removed_bookmarks(buf_name) 284 | 285 | _sync_missing_bookmarks(buf, buf_name) 286 | 287 | -- marshal any updated node positions. 288 | self.tree.marshal({ no_guides = true }) 289 | end 290 | 291 | -- Sync the current buffer with any available bookmarks, creating a right 292 | -- aligned virtual text chunk with the bookmark details. 293 | function self.buf_sync_bookmarks() 294 | local buf = vim.api.nvim_get_current_buf() 295 | local buf_name = vim.api.nvim_buf_get_name(buf) 296 | _buf_sync_bookmarks(buf, buf_name) 297 | end 298 | 299 | self.sync_aucmd = vim.api.nvim_create_autocmd({ "BufEnter", "CursorHold", "CursorHoldI" }, { 300 | callback = function() 301 | if not libws.is_current_ws(self.component.workspace) then 302 | return 303 | end 304 | self.buf_sync_bookmarks() 305 | end, 306 | }) 307 | 308 | self.write_aucmd = vim.api.nvim_create_autocmd({ "BufWrite" }, { 309 | callback = function(args) 310 | self.write_bookmarks(args.file) 311 | end, 312 | }) 313 | 314 | return self 315 | end 316 | 317 | return Notebook 318 | --------------------------------------------------------------------------------