From 7450625ba86949afbe5932bbe7798a4d5a2b75a1 Mon Sep 17 00:00:00 2001 From: pojok code Date: Sun, 22 Jun 2025 08:43:35 +0700 Subject: [PATCH] enc: update modal npm runner --- lua/pcode/plugins/conform.lua | 79 +++---- lua/pcode/plugins/lang/javascript.lua | 21 +- lua/pcode/plugins/notify.lua | 1 + lua/pcode/user/npmrun.lua | 316 ++++++++++++++++---------- lua/pcode/user/npmrun3.lua | 174 ++++++++++++++ lua/pcode/user/npmrun4.lua | 158 +++++++++++++ 6 files changed, 588 insertions(+), 161 deletions(-) create mode 100644 lua/pcode/user/npmrun3.lua create mode 100644 lua/pcode/user/npmrun4.lua diff --git a/lua/pcode/plugins/conform.lua b/lua/pcode/plugins/conform.lua index a674af9..7d4d5a3 100644 --- a/lua/pcode/plugins/conform.lua +++ b/lua/pcode/plugins/conform.lua @@ -1,40 +1,43 @@ return { - "pojokcodeid/auto-conform.nvim", - dependencies = { - "williamboman/mason.nvim", - "stevearc/conform.nvim", - }, - event = "VeryLazy", - opts = function(_, opts) - opts.formatters = opts.formatters or {} - opts.formatters_by_ft = opts.formatters_by_ft or {} - opts.ensure_installed = opts.ensure_installed or {} - -- vim.list_extend(opts.ensure_installed, { "stylua" }) - opts.lang_maps = opts.lang_maps or {} - opts.name_maps = opts.name_maps or {} - opts.add_new = opts.add_new or {} - opts.ignore = opts.ignore or {} - opts.format_on_save = opts.format_on_save or true - opts.format_timeout_ms = opts.format_timeout_ms or 5000 - end, - config = function(_, opts) - require("auto-conform").setup(opts) - -- other conform config - local conform = require("conform") - if opts.format_on_save then - conform.setup({ - format_on_save = { - lsp_fallback = true, - timeout_ms = opts.format_timeout_ms or 5000, - }, - }) - end - vim.keymap.set({ "n", "v" }, "lF", function() - conform.format({ - lsp_fallback = true, - async = false, - timeout_ms = opts.format_timeout_ms or 5000, - }) - end, { desc = "Format file or range (in visual mode)" }) - end, + "pojokcodeid/auto-conform.nvim", + dependencies = { + "williamboman/mason.nvim", + "stevearc/conform.nvim", + }, + event = "VeryLazy", + opts = function(_, opts) + opts.formatters = opts.formatters or {} + opts.formatters_by_ft = opts.formatters_by_ft or {} + opts.ensure_installed = opts.ensure_installed or {} + -- vim.list_extend(opts.ensure_installed, { "stylua" }) + opts.lang_maps = opts.lang_maps or {} + opts.name_maps = opts.name_maps or {} + opts.add_new = opts.add_new or {} + opts.ignore = opts.ignore or {} + opts.format_on_save = opts.format_on_save or true + opts.format_timeout_ms = opts.format_timeout_ms or 5000 + end, + config = function(_, opts) + require("auto-conform").setup(opts) + -- other conform config + local conform = require("conform") + if opts.format_on_save then + conform.setup({ + format_on_save = { + lsp_fallback = true, + timeout_ms = opts.format_timeout_ms or 5000, + }, + }) + end + vim.keymap.set({ "n", "v" }, "lF", function() + conform.format({ + lsp_fallback = true, + async = false, + timeout_ms = opts.format_timeout_ms or 5000, + }) + end, { desc = "Format file or range (in visual mode)" }) + end, + keys = { + { "l", "", desc = " 󰘦 Lsp" }, + }, } diff --git a/lua/pcode/plugins/lang/javascript.lua b/lua/pcode/plugins/lang/javascript.lua index fd637a3..271f8bc 100644 --- a/lua/pcode/plugins/lang/javascript.lua +++ b/lua/pcode/plugins/lang/javascript.lua @@ -94,10 +94,29 @@ local M = { end require("neotest").setup(opts) - require("pcode.user.npmrun").setup("npm run dev") + local command = pcode.npm_commad + or { + dev = { + start = "NpmRunDev", + stop = "NpmStopDev", + cmd = "npm run dev", + }, + prod = { + start = "NpmStart", + stop = "NpmStop", + cmd = "npm start", + }, + } + require("pcode.user.npmrun").setup(command, { + show_mapping = "nm", + hide_mapping = "nh", + width = 70, + height = 20, + }) end, -- stylua: ignore keys = { + {"n","",desc="  Npm"}, { "T","",desc="  Test"}, { "Tt", function() require("neotest").run.run(vim.fn.expand("%")) end, desc = "Run File" }, { "Tr", function() require("neotest").run.run() end, desc = "Run Nearest" }, diff --git a/lua/pcode/plugins/notify.lua b/lua/pcode/plugins/notify.lua index ba74835..9030dec 100644 --- a/lua/pcode/plugins/notify.lua +++ b/lua/pcode/plugins/notify.lua @@ -3,6 +3,7 @@ return { lazy = true, event = "VeryLazy", keys = { + { "u", "", desc = " 󰀦 Notify" }, { "uN", function() diff --git a/lua/pcode/user/npmrun.lua b/lua/pcode/user/npmrun.lua index 79b0724..495468a 100644 --- a/lua/pcode/user/npmrun.lua +++ b/lua/pcode/user/npmrun.lua @@ -1,52 +1,112 @@ -local function show_modal(text) - local buf = vim.api.nvim_create_buf(false, true) -- buffer untuk modal +local last_modal = nil -- global modal manager instance - vim.api.nvim_buf_set_lines(buf, 0, -1, false, { text, "", "Press q to close" }) +local function modal_manager(opts) + opts = opts or {} + local win_id = nil + local buf_id = nil + local last_content = "" + local is_shown = false - local width = 50 - local height = 5 - local row = math.floor((vim.o.lines - height) / 2) - local col = math.floor((vim.o.columns - width) / 2) + local function close() + if win_id and vim.api.nvim_win_is_valid(win_id) then + vim.api.nvim_win_close(win_id, true) + win_id = nil + end + is_shown = false + end - local win = vim.api.nvim_open_win(buf, true, { - relative = "editor", - width = width, - height = height, - row = row, - col = col, - style = "minimal", - border = "rounded", - }) + local function open(content) + local lines = vim.split(content or "", "\n") + local width = opts.width or 70 + local height = math.min(opts.height or 15, #lines + 6) + local row = math.floor((vim.o.lines - height) / 2) + local col = math.floor((vim.o.columns - width) / 2) + if not buf_id or not vim.api.nvim_buf_is_valid(buf_id) then + buf_id = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_keymap(buf_id, "n", "q", "", { + noremap = true, + silent = true, + callback = function() + close() + end, + }) + end + -- Pastikan buffer modifiable sebelum update, abaikan jika error (buffer deleted) + pcall(vim.api.nvim_buf_set_option, buf_id, "modifiable", true) + pcall(vim.api.nvim_buf_set_lines, buf_id, 0, -1, false, lines) + pcall(vim.api.nvim_buf_set_option, buf_id, "modifiable", false) + if not win_id or not vim.api.nvim_win_is_valid(win_id) then + win_id = vim.api.nvim_open_win(buf_id, true, { + relative = "editor", + width = width, + height = height, + row = row, + col = col, + style = "minimal", + border = "rounded", + }) + end + is_shown = true + end - -- keymap untuk menutup modal dengan 'q' - vim.api.nvim_buf_set_keymap(buf, "n", "q", "", { - noremap = true, - silent = true, - callback = function() - vim.api.nvim_win_close(win, true) + local function show() + if is_shown then + return + end + if last_content ~= "" then + open(last_content) + end + end + + local function hide() + close() + end + + local function update(content) + last_content = content + if is_shown and buf_id and vim.api.nvim_buf_is_valid(buf_id) then + local lines = vim.split(content or "", "\n") + pcall(vim.api.nvim_buf_set_option, buf_id, "modifiable", true) + pcall(vim.api.nvim_buf_set_lines, buf_id, 0, -1, false, lines) + pcall(vim.api.nvim_buf_set_option, buf_id, "modifiable", false) + end + end + + local function set_content(content) + last_content = content + if is_shown then + update(content) + else + open(content) + end + end + + return { + show = show, + hide = hide, + update = update, + close = close, + set_content = set_content, + is_shown = function() + return is_shown end, - }) + } end local M = {} - -local main_cmd = "npm run dev" -- default +M._last_modal = nil local function log(message, level) vim.notify(string.format("npm-dev-runner: %s", message), vim.log.levels[level]) end --- Cache: dir -> { job_id=..., buf=... } -local job_cache = {} - -local function find_cached_dir(dir) +local function find_cached_dir(dir, cache) if not dir then vim.notify("npm-dev-runner: No directory provided to find_cached_dir()", vim.log.levels.ERROR) return end - local cur = dir - while not job_cache[cur] do + while not cache[cur] do if cur == "/" or string.match(cur, "^[A-Z]:\\$") then return end @@ -55,119 +115,131 @@ local function find_cached_dir(dir) return cur end -local function is_running(dir) - local cached_dir = find_cached_dir(dir) - return cached_dir and job_cache[cached_dir] +local function is_running(dir, cache) + local cached_dir = find_cached_dir(dir, cache) + return cached_dir and cache[cached_dir] end local function is_windows() return vim.loop.os_uname().version:match("Windows") end -M.toggle = function(dir) - local running = is_running(dir) - if not running then - M.start(dir) - return - end - M.stop(dir) -end +local default_opts = { + show_mapping = "nm", + hide_mapping = "nh", +} ---- Fungsi setup menerima argumen command utama, contoh: require("npmrun").setup("pnpm dev") -M.setup = function(cmd) - main_cmd = cmd or "npm run dev" - if not vim.fn.executable(main_cmd:match("%S+")) then - log(main_cmd .. " is not executable. Make sure it is installed and in PATH.", "ERROR") - return +M.setup = function(command_table, opts) + opts = vim.tbl_deep_extend("force", {}, default_opts, opts or {}) + command_table = command_table or {} + + -- Keymap global, pakai modal terakhir yang aktif + if opts.show_mapping then + vim.keymap.set("n", opts.show_mapping, function() + if last_modal then + last_modal.show() + end + end, { desc = "Show last NPM modal output" }) + end + if opts.hide_mapping then + vim.keymap.set("n", opts.hide_mapping, function() + if last_modal then + last_modal.hide() + end + end, { desc = "Hide last NPM modal output" }) end - local function find_dir(args) - local dir = args ~= "" and args or "%:p:h" - return vim.fn.expand(vim.fn.fnamemodify(vim.fn.expand(dir), ":p")) - end + for key, conf in pairs(command_table) do + local start_cmd = conf.start or ("NpmRun" .. key) + local stop_cmd = conf.stop or ("NpmStop" .. key) + local cmd_str = conf.cmd or "npm run dev" + local cache = {} - vim.api.nvim_create_user_command("DevStart", function(opts) - M.start(find_dir(opts.args)) - end, { nargs = "?" }) + local function do_start(dir) + if is_running(dir, cache) then + log(cmd_str .. " already running", "INFO") + return + end - vim.api.nvim_create_user_command("DevStop", function(opts) - M.stop(find_dir(opts.args)) - end, { nargs = "?" }) + local all_output = {} + local modal = modal_manager(opts) + last_modal = modal + M._last_modal = modal - vim.api.nvim_create_user_command("DevToggle", function(opts) - M.toggle(find_dir(opts.args)) - end, { nargs = "?" }) -end + local cmd + if is_windows() then + cmd = { "cmd.exe", "/C" } + for word in cmd_str:gmatch("%S+") do + table.insert(cmd, word) + end + else + cmd = {} + for word in cmd_str:gmatch("%S+") do + table.insert(cmd, word) + end + end -M.start = function(dir) - if is_running(dir) then - log(main_cmd .. " already running", "INFO") - return - end + local function process_lines(lines) + if not lines then + return + end + for _, l in ipairs(lines) do + table.insert(all_output, tostring(l)) + end + modal.set_content(table.concat(all_output, "\n")) + end - local cmd - if is_windows() then - cmd = { "cmd.exe", "/C" } - for word in main_cmd:gmatch("%S+") do - table.insert(cmd, word) - end - else - cmd = {} - for word in main_cmd:gmatch("%S+") do - table.insert(cmd, word) - end - end + local job_id = vim.fn.jobstart(cmd, { + cwd = dir, + stdout_buffered = false, + stderr_buffered = false, + on_stdout = function(_, data) + process_lines(data) + end, + on_stderr = function(_, data) + process_lines(data) + end, + on_exit = function(_, code) + table.insert(all_output, ("Process exited with code: %d"):format(code)) + modal.set_content(table.concat(all_output, "\n")) + cache[dir] = nil + end, + }) - local function append_to_buffer(lines) - if not lines then - return + cache[dir] = { job_id = job_id, modal = modal } + log(cmd_str .. " started", "INFO") end - for _, line in ipairs(lines) do - if line ~= "" then - line = tostring(line) - line = line:gsub("^%s*(.-)%s*$", "%1") - if string.find(line, "http") then - show_modal(line) - break + local function do_stop(dir) + local running = is_running(dir, cache) + if running then + local cached_dir = find_cached_dir(dir, cache) + if cached_dir then + local job_entry = cache[cached_dir] + if job_entry then + vim.fn.jobstop(job_entry.job_id) + if job_entry.modal then + job_entry.modal.close() + end + end + cache[cached_dir] = nil + log(cmd_str .. " stopped", "INFO") end end end - end - local job_id = vim.fn.jobstart(cmd, { - cwd = dir, - stdout_buffered = false, -- streaming mode - stderr_buffered = false, - on_stdout = function(_, data) - append_to_buffer(data) - end, - on_stderr = function(_, data) - append_to_buffer(vim.tbl_map(function(l) - return "[ERR] " .. l - end, data)) - end, - on_exit = function(_, _) - job_cache[dir] = nil - end, - }) - - job_cache[dir] = { job_id = job_id } - log(main_cmd .. " started", "INFO") -end - -M.stop = function(dir) - local running = is_running(dir) - if running then - local cached_dir = find_cached_dir(dir) - if cached_dir then - local job_entry = job_cache[cached_dir] - if job_entry then - vim.fn.jobstop(job_entry.job_id) - end - job_cache[cached_dir] = nil - log(main_cmd .. " stopped", "INFO") + local function find_dir(args) + local dir = args ~= "" and args or "%:p:h" + return vim.fn.expand(vim.fn.fnamemodify(vim.fn.expand(dir), ":p")) end + + vim.api.nvim_create_user_command(start_cmd, function(opts) + do_start(find_dir(opts.args)) + end, { nargs = "?" }) + + vim.api.nvim_create_user_command(stop_cmd, function(opts) + do_stop(find_dir(opts.args)) + end, { nargs = "?" }) end end diff --git a/lua/pcode/user/npmrun3.lua b/lua/pcode/user/npmrun3.lua new file mode 100644 index 0000000..79b0724 --- /dev/null +++ b/lua/pcode/user/npmrun3.lua @@ -0,0 +1,174 @@ +local function show_modal(text) + local buf = vim.api.nvim_create_buf(false, true) -- buffer untuk modal + + vim.api.nvim_buf_set_lines(buf, 0, -1, false, { text, "", "Press q to close" }) + + local width = 50 + local height = 5 + local row = math.floor((vim.o.lines - height) / 2) + local col = math.floor((vim.o.columns - width) / 2) + + local win = vim.api.nvim_open_win(buf, true, { + relative = "editor", + width = width, + height = height, + row = row, + col = col, + style = "minimal", + border = "rounded", + }) + + -- keymap untuk menutup modal dengan 'q' + vim.api.nvim_buf_set_keymap(buf, "n", "q", "", { + noremap = true, + silent = true, + callback = function() + vim.api.nvim_win_close(win, true) + end, + }) +end + +local M = {} + +local main_cmd = "npm run dev" -- default + +local function log(message, level) + vim.notify(string.format("npm-dev-runner: %s", message), vim.log.levels[level]) +end + +-- Cache: dir -> { job_id=..., buf=... } +local job_cache = {} + +local function find_cached_dir(dir) + if not dir then + vim.notify("npm-dev-runner: No directory provided to find_cached_dir()", vim.log.levels.ERROR) + return + end + + local cur = dir + while not job_cache[cur] do + if cur == "/" or string.match(cur, "^[A-Z]:\\$") then + return + end + cur = vim.fn.fnamemodify(cur, ":h") + end + return cur +end + +local function is_running(dir) + local cached_dir = find_cached_dir(dir) + return cached_dir and job_cache[cached_dir] +end + +local function is_windows() + return vim.loop.os_uname().version:match("Windows") +end + +M.toggle = function(dir) + local running = is_running(dir) + if not running then + M.start(dir) + return + end + M.stop(dir) +end + +--- Fungsi setup menerima argumen command utama, contoh: require("npmrun").setup("pnpm dev") +M.setup = function(cmd) + main_cmd = cmd or "npm run dev" + if not vim.fn.executable(main_cmd:match("%S+")) then + log(main_cmd .. " is not executable. Make sure it is installed and in PATH.", "ERROR") + return + end + + local function find_dir(args) + local dir = args ~= "" and args or "%:p:h" + return vim.fn.expand(vim.fn.fnamemodify(vim.fn.expand(dir), ":p")) + end + + vim.api.nvim_create_user_command("DevStart", function(opts) + M.start(find_dir(opts.args)) + end, { nargs = "?" }) + + vim.api.nvim_create_user_command("DevStop", function(opts) + M.stop(find_dir(opts.args)) + end, { nargs = "?" }) + + vim.api.nvim_create_user_command("DevToggle", function(opts) + M.toggle(find_dir(opts.args)) + end, { nargs = "?" }) +end + +M.start = function(dir) + if is_running(dir) then + log(main_cmd .. " already running", "INFO") + return + end + + local cmd + if is_windows() then + cmd = { "cmd.exe", "/C" } + for word in main_cmd:gmatch("%S+") do + table.insert(cmd, word) + end + else + cmd = {} + for word in main_cmd:gmatch("%S+") do + table.insert(cmd, word) + end + end + + local function append_to_buffer(lines) + if not lines then + return + end + + for _, line in ipairs(lines) do + if line ~= "" then + line = tostring(line) + line = line:gsub("^%s*(.-)%s*$", "%1") + if string.find(line, "http") then + show_modal(line) + break + end + end + end + end + + local job_id = vim.fn.jobstart(cmd, { + cwd = dir, + stdout_buffered = false, -- streaming mode + stderr_buffered = false, + on_stdout = function(_, data) + append_to_buffer(data) + end, + on_stderr = function(_, data) + append_to_buffer(vim.tbl_map(function(l) + return "[ERR] " .. l + end, data)) + end, + on_exit = function(_, _) + job_cache[dir] = nil + end, + }) + + job_cache[dir] = { job_id = job_id } + log(main_cmd .. " started", "INFO") +end + +M.stop = function(dir) + local running = is_running(dir) + if running then + local cached_dir = find_cached_dir(dir) + if cached_dir then + local job_entry = job_cache[cached_dir] + if job_entry then + vim.fn.jobstop(job_entry.job_id) + end + job_cache[cached_dir] = nil + log(main_cmd .. " stopped", "INFO") + end + end +end + +return M diff --git a/lua/pcode/user/npmrun4.lua b/lua/pcode/user/npmrun4.lua new file mode 100644 index 0000000..1c8c4f1 --- /dev/null +++ b/lua/pcode/user/npmrun4.lua @@ -0,0 +1,158 @@ +local function show_modal(text) + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, { text, "", "Press q to close" }) + + local width = 50 + local height = 5 + local row = math.floor((vim.o.lines - height) / 2) + local col = math.floor((vim.o.columns - width) / 2) + + local win = vim.api.nvim_open_win(buf, true, { + relative = "editor", + width = width, + height = height, + row = row, + col = col, + style = "minimal", + border = "rounded", + }) + + vim.api.nvim_buf_set_keymap(buf, "n", "q", "", { + noremap = true, + silent = true, + callback = function() + vim.api.nvim_win_close(win, true) + end, + }) +end + +local M = {} + +local function log(message, level) + vim.notify(string.format("npm-dev-runner: %s", message), vim.log.levels[level]) +end + +-- Fungsi untuk mencari cache dir (supaya stop/start tetap per dir/project) +local function find_cached_dir(dir, cache) + if not dir then + vim.notify("npm-dev-runner: No directory provided to find_cached_dir()", vim.log.levels.ERROR) + return + end + local cur = dir + while not cache[cur] do + if cur == "/" or string.match(cur, "^[A-Z]:\\$") then + return + end + cur = vim.fn.fnamemodify(cur, ":h") + end + return cur +end + +-- Fungsi cek proses running +local function is_running(dir, cache) + local cached_dir = find_cached_dir(dir, cache) + return cached_dir and cache[cached_dir] +end + +local function is_windows() + return vim.loop.os_uname().version:match("Windows") +end + +M.setup = function(command_table) + command_table = command_table or {} + for key, conf in pairs(command_table) do + local start_cmd = conf.start or ("NpmRun" .. key) + local stop_cmd = conf.stop or ("NpmStop" .. key) + local cmd_str = conf.cmd or "npm run dev" + + -- cache khusus untuk mode ini + local cache = {} + + -- Fungsi Start + local function do_start(dir) + if is_running(dir, cache) then + log(cmd_str .. " already running", "INFO") + return + end + + local cmd + if is_windows() then + cmd = { "cmd.exe", "/C" } + for word in cmd_str:gmatch("%S+") do + table.insert(cmd, word) + end + else + cmd = {} + for word in cmd_str:gmatch("%S+") do + table.insert(cmd, word) + end + end + + local function process_lines(lines) + if not lines then + return + end + for _, line in ipairs(lines) do + if line ~= "" then + local str = tostring(line):gsub("^%s*(.-)%s*$", "%1") + if string.find(str, "http") then + show_modal(str) + end + end + end + end + + local job_id = vim.fn.jobstart(cmd, { + cwd = dir, + stdout_buffered = false, + stderr_buffered = false, + on_stdout = function(_, data) + process_lines(data) + end, + on_stderr = function(_, data) + process_lines(vim.tbl_map(function(l) + return "[ERR] " .. l + end, data)) + end, + on_exit = function(_, _) + cache[dir] = nil + end, + }) + + cache[dir] = { job_id = job_id } + log(cmd_str .. " started", "INFO") + end + + -- Fungsi Stop + local function do_stop(dir) + local running = is_running(dir, cache) + if running then + local cached_dir = find_cached_dir(dir, cache) + if cached_dir then + local job_entry = cache[cached_dir] + if job_entry then + vim.fn.jobstop(job_entry.job_id) + end + cache[cached_dir] = nil + log(cmd_str .. " stopped", "INFO") + end + end + end + + local function find_dir(args) + local dir = args ~= "" and args or "%:p:h" + return vim.fn.expand(vim.fn.fnamemodify(vim.fn.expand(dir), ":p")) + end + + -- Register Perintah + vim.api.nvim_create_user_command(start_cmd, function(opts) + do_start(find_dir(opts.args)) + end, { nargs = "?" }) + + vim.api.nvim_create_user_command(stop_cmd, function(opts) + do_stop(find_dir(opts.args)) + end, { nargs = "?" }) + end +end + +return M