commit 4037e24558561b5a880eac64adb6e65cb0f71ad7 Author: Barrett Ruth Date: Sat Dec 31 14:04:00 2022 -0600 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c3ac1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/import-cost/ +doc/tags +vim.toml +selene.toml diff --git a/.stylua.toml b/.stylua.toml new file mode 100644 index 0000000..54e6142 --- /dev/null +++ b/.stylua.toml @@ -0,0 +1,4 @@ +quote_style = "AutoPreferSingle" +call_parentheses = "None" +indent_type = "Spaces" +column_width = 80 diff --git a/doc/import-cost.txt b/doc/import-cost.txt new file mode 100644 index 0000000..f727d4a --- /dev/null +++ b/doc/import-cost.txt @@ -0,0 +1,49 @@ +*import-cost.nvim* + +Author: Barrett Ruth +Homepage: + +=============================================================================== +INTRODUCTION *import-cost.nvim* + +import-cost.nvim displays the costs of javascript imports inside neovim. +It works with ES6 and CommonJS modules in any javascript, javascriptreact, +typescript, or typescriptreact files. + +Author: Barrett Ruth + +=============================================================================== +SETUP *import-cost.setup()* +>lua + require('import-cost').setup(config) +< +Module setup. + +Parameters: ~ + + {config} `(table)`: Table containing configuration for import-cost. + +Usage: ~ +>lua + require('import-cost').setup({ + -- Filetype to attach to + filetypes = { + 'javascript', + 'javascriptreact', + 'typescript', + 'typescriptreact', + }, + format = { + -- Format string for bytes/kilobytes in virtual text + byte_format = '%.2f b', + kb_format = '%.2f kb', + -- Virtual text format (remove second "%s" to ignore gzipped size) + virtual_text = '%s (gzipped: %s)', + }, + -- Highlight of virtual text — + -- a highlight group to link to or table as specified by nvim_set_hl() + highlight = 'Comment', + }) +< +------------------------------------------------------------------------------- +vim:tw=80:ts=8:ft=help: diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..cfd762f --- /dev/null +++ b/install.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +git clone 'git@github.com:wix/import-cost.git' || (echo 'Failed to clone wix/import-cost' && exit) + +echo 'Install import-cost with which package manager? [npm/yarn]' +read -r installer + +if [ "$installer" != 'npm' ] && [ "$installer" != 'yarn' ]; then + echo "Please enter either 'npm' or 'yarn'" + exit +fi + +cd import-cost || exit +eval "$installer install" +cd .. + +cp src/index.js import-cost diff --git a/lua/import-cost.lua b/lua/import-cost.lua new file mode 100644 index 0000000..89730f4 --- /dev/null +++ b/lua/import-cost.lua @@ -0,0 +1,64 @@ +local M = {} + +local function is_ic_buf(bufnr) + local filetype = vim.api.nvim_buf_get_option(bufnr, 'filetype') + + return vim.tbl_contains(M.config.filetypes, filetype) +end + +local function au(events, cb) + vim.api.nvim_create_autocmd(events, { + callback = function(opts) + if is_ic_buf(opts.buf) then + cb(opts) + end + end, + group = M.aug_id, + }) +end + +M.config = { + filetypes = { + 'javascript', + 'javascriptreact', + 'typescript', + 'typescriptreact', + }, + format = { + byte_format = '%.2f b', + kb_format = '%.2f kb', + virtual_text = '%s (gzipped: %s)', + }, + highlight = 'Comment', +} + +M.setup = function(user_config) + M.config = vim.tbl_deep_extend('force', M.config, user_config or {}) + + M.ns_id = vim.api.nvim_create_namespace 'ImportCost' + + M.script_path = vim.fn.fnamemodify(debug.getinfo(1).source:sub(2), ':h:h') + .. '/import-cost/index.js' + + vim.api.nvim_set_hl( + 0, + 'ImportCostVirtualText', + ---@diagnostic disable-next-line: param-type-mismatch + type(M.config.highlight) == 'string' and { link = M.config.highlight } + or M.config.highlight + ) + + M.aug_id = vim.api.nvim_create_augroup('ImportCost', {}) + + local extmark = require 'import-cost.extmark' + + au({ 'BufEnter', 'BufWritePost' }, function(opts) + extmark.set_missing_extmarks(opts.buf) + end) + + au('TextChanged', function(opts) + extmark.update_extmarks(opts.buf) + end) +end + +return M diff --git a/lua/import-cost/extmark.lua b/lua/import-cost/extmark.lua new file mode 100644 index 0000000..28bce0d --- /dev/null +++ b/lua/import-cost/extmark.lua @@ -0,0 +1,104 @@ +local M = {} + +local ic, job, util = + require 'import-cost', require 'import-cost.job', require 'import-cost.util' + +local visible_strings = {} + +local function update_visible_strings(bufnr, data, extmark_id) + if not visible_strings[bufnr] then + visible_strings[bufnr] = {} + end + + visible_strings[bufnr][data.string] = data + visible_strings[bufnr][data.string].extmark_id = extmark_id +end + +local function string_visible(bufnr, string) + return visible_strings[bufnr] and visible_strings[bufnr][string] +end + +function M.set_missing_extmarks(bufnr) + local path = vim.api.nvim_buf_get_name(bufnr) + local filetype = vim.api.nvim_buf_get_option(bufnr, 'filetype') + + local cmd = { 'node', ic.script_path, path, filetype } + + local job_id = vim.fn.jobstart(cmd, { + on_stdout = function(_, stdout, _) + if not stdout or stdout[1] == '' then + return + end + + local chunks = vim.split(stdout[1], '|', { trimempty = true }) + + for _, chunk in ipairs(chunks) do + local data = util.parse_data(chunk) + + if util.is_ok(data) then + local string = util.normalize_string(data.string) + + if not string_visible(bufnr, string) then + data.string = string + + local extmark_id = job.set_extmark(bufnr, data) + + update_visible_strings(bufnr, data, extmark_id) + end + end + end + end, + }) + + job.stop_prev_job(job_id, bufnr) + job.send_buf_contents(job_id, bufnr) +end + +function M.clear_extmarks(bufnr) + if visible_strings[bufnr] then + visible_strings[bufnr] = nil + end + + vim.api.nvim_buf_clear_namespace(bufnr, ic.ns_id, 0, -1) +end + +function M.update_extmarks(bufnr) + local buffer_strings = M.move_existing_extmarks(bufnr) + M.delete_remaining_extmarks(bufnr, buffer_strings) + M.set_missing_extmarks(bufnr) +end + +function M.move_existing_extmarks(bufnr) + local buffer_strings = {} + + for nr, raw_string in ipairs(vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)) do + if util.is_import_string(raw_string) then + local string = util.normalize_string(raw_string) + local data = visible_strings[bufnr][string] + + buffer_strings[string] = true + + if data and data.string == string then + if data.line ~= nr then + data.line = nr + + job.set_extmark(bufnr, data, data.extmark_id) + end + end + end + end + + return buffer_strings +end + +function M.delete_remaining_extmarks(bufnr, buffer_strings) + for string, data in pairs(visible_strings[bufnr]) do + if not buffer_strings[string] then + vim.api.nvim_buf_del_extmark(bufnr, ic.ns_id, data.extmark_id) + + visible_strings[bufnr][string] = nil + end + end +end + +return M diff --git a/lua/import-cost/import.lua b/lua/import-cost/import.lua new file mode 100644 index 0000000..5044d6f --- /dev/null +++ b/lua/import-cost/import.lua @@ -0,0 +1,3 @@ +local M = {} + +return M diff --git a/lua/import-cost/job.lua b/lua/import-cost/job.lua new file mode 100644 index 0000000..e4fb2d5 --- /dev/null +++ b/lua/import-cost/job.lua @@ -0,0 +1,48 @@ +local M = {} + +local ic = require 'import-cost' +local format = ic.config.format + +local job_cache = {} + +local format_bytes = function(bytes) + if bytes < 1024 then + return string.format(format.byte_format, bytes) + end + + return string.format(format.kb_format, 0.0009765625 * bytes) +end + +function M.set_extmark(bufnr, data, extmark_id) + local line, size, gzip = + data.line - 1, format_bytes(data.size), format_bytes(data.gzip) + + local virt_text = string.format(format.virtual_text, size, gzip) + + return vim.api.nvim_buf_set_extmark(bufnr, ic.ns_id, line, -1, { + id = extmark_id, + virt_text = { + { + virt_text, + 'ImportCostVirtualText', + }, + }, + }) +end + +function M.send_buf_contents(job_id, bufnr) + local contents = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) + + vim.fn.chansend(job_id, contents) + vim.fn.chanclose(job_id, 'stdin') +end + +function M.stop_prev_job(job_id, bufnr) + if job_cache[bufnr] then + vim.fn.jobstop(job_cache[bufnr]) + end + + job_cache[bufnr] = job_id +end + +return M diff --git a/lua/import-cost/util.lua b/lua/import-cost/util.lua new file mode 100644 index 0000000..91a9f76 --- /dev/null +++ b/lua/import-cost/util.lua @@ -0,0 +1,39 @@ +local M = {} + +function M.is_import_string(string) + return string:sub(1, 6) == 'import' or string:sub(1, 5) == 'const' +end + +function M.normalize_string(raw_string) + local string = raw_string + + -- pad with semicolon + if string:sub(-1, -1) ~= ';' then + string = string .. ';' + end + + -- stylua: ignore + string = string + :match('.-;') -- extract first statement + :gsub(' as %S+', '') -- remove aliases + :gsub("'", '"') -- swap single for double quotes + :gsub('%s+', '') -- remove whitespace + + return string +end + +function M.parse_data(chunk) + local ok, json = pcall(vim.fn.json_decode, chunk) + + if not ok or not json then + return + end + + return json.data +end + +function M.is_ok(data) + return data and data.size +end + +return M diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..9f82e2a --- /dev/null +++ b/readme.md @@ -0,0 +1,36 @@ +# import-cost.nvim + +Display the costs of javascript imports inside neovim with the power of +[import-cost](https://github.com/wix/import-cost) + +![preview](https://user-images.githubusercontent.com/62671086/210154626-1dc76271-c063-4a37-962f-9c4d86593421.png) + +## Installation + +1. Install regularly with your package manager +2. Run the install script inside the directory: + +```sh +sh install.sh +``` + +Example configuration with [packer.nvim](https://github.com/wbthomason/packer.nvim): + +```lua +use { + 'barrett-ruth/import-cost.nvim', + run = 'sh install.sh' +} +``` + +## Configuration + +```lua +require('import-cost').setup(opts) +``` + +See `:h import-cost` for more information + +## TODO + +- Automatic setup diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..bc2b9ed --- /dev/null +++ b/src/index.js @@ -0,0 +1,52 @@ +const { cleanup, importCost, Lang } = require("import-cost"); + +process.stdin.setEncoding("utf8"); + +const receive = async () => { + let result = ""; + + for await (const chunk of process.stdin) result += chunk; + + return result; +}; + +// Pad data with '|' for combined chunks to be parsable +const give = (data) => + process.nextTick(() => process.stdout.write(`${JSON.stringify(data)}|`)); + +const init = async () => { + const [path, filetype] = process.argv.slice(2); + const lang = + filetype.substring(0, "typescript".length) === "typescript" + ? Lang.TYPESCRIPT + : Lang.JAVASCRIPT; + const contents = await receive(); + + const emitter = importCost(path, contents, lang); + + emitter.on("error", (error) => { + give({ type: "error", error }); + + cleanup(); + }); + + emitter.on("calculated", ({ line, string, size, gzip }) => { + give({ + type: "calculated", + data: { line, string, size, gzip }, + }); + }); + + // Send done to ensure job stdin stays open + emitter.on("done", (_) => { + give({ + type: "done", + }); + + cleanup(); + }); +}; + +try { + init(); +} catch {}