log.fnl
Branches > main > d3c0a10 > files / nvim / pack / plugins / opt / conjure / fnl / conjure / log.fnl
(local {: autoload : define} (require :conjure.nfnl.module))
(local core (autoload :conjure.nfnl.core))
(local str (autoload :conjure.nfnl.string))
(local buffer (autoload :conjure.buffer))
(local client (autoload :conjure.client))
(local hook (autoload :conjure.hook))
(local config (autoload :conjure.config))
(local text (autoload :conjure.text))
(local editor (autoload :conjure.editor))
(local timer (autoload :conjure.timer))
(local sponsors (require :conjure.sponsors))
(local vim _G.vim)
(local M (define :conjure.log))
(set M.state
(or
M.state
{:last-open-cmd :vsplit
:buffers {}
:hud {:id nil
:timer nil
:created-at-ms 0
:eval-seen? false
:low-priority-spam {:streak 0
:help-displayed? false}}
:jump-to-latest {:mark nil
:ns (vim.api.nvim_create_namespace "conjure_log_jump_to_latest")}}))
(fn break []
(str.join
[(client.get :comment-prefix)
(string.rep "-" (config.get-in [:log :break_length]))]))
(fn state-key-header []
(str.join [(client.get :comment-prefix) "State: " (client.state-key)]))
(fn log-buf-name []
(str.join
["conjure-log-"
(if (and (config.get-in [:log :linked_to_client_state])
(client.multiple-states?))
(client.state-key)
(vim.fn.getpid))
(client.get :buf-suffix)]))
(fn M.log-buf? [name]
(vim.endswith name (log-buf-name)))
(fn on-new-log-buf [buf]
(set M.state.jump-to-latest.mark
(vim.api.nvim_buf_set_extmark buf M.state.jump-to-latest.ns 0 0 {}))
(when (and vim.diagnostic (= false (config.get-in [:log :diagnostics])))
(if (= 1 (vim.fn.has "nvim-0.10"))
(vim.diagnostic.enable false {:bufnr buf})
(vim.diagnostic.disable buf)))
(when (and vim.treesitter (= false (config.get-in [:log :treesitter])))
(vim.treesitter.stop buf)
(tset vim.bo buf :syntax "on"))
(math.randomseed (os.time))
(vim.api.nvim_buf_set_lines
buf 0 -1 false
[(str.join [(client.get :comment-prefix)
"Sponsored by @"
(core.get sponsors (core.inc (math.floor (core.rand (core.dec (core.count sponsors))))))
" ❤"])]))
(fn upsert-buf []
(buffer.upsert-hidden
(log-buf-name)
(client.wrap on-new-log-buf)))
(fn M.clear-close-hud-passive-timer []
(core.update-in M.state [:hud :timer] timer.destroy))
(hook.define
:close-hud
(fn []
(when M.state.hud.id
(pcall vim.api.nvim_win_close M.state.hud.id true)
(set M.state.hud.id nil))))
(fn M.close-hud []
(M.clear-close-hud-passive-timer)
(hook.exec :close-hud))
(fn M.hud-lifetime-ms []
(- (vim.uv.now) M.state.hud.created-at-ms))
(fn M.close-hud-passive []
(when (and M.state.hud.id
(> (M.hud-lifetime-ms)
(config.get-in [:log :hud :minimum_lifetime_ms])))
(let [delay (config.get-in [:log :hud :passive_close_delay])]
(if (= 0 delay)
(M.close-hud)
(when (not (core.get-in M.state [:hud :timer]))
(core.assoc-in
M.state [:hud :timer]
(timer.defer M.close-hud delay)))))))
(fn break-lines [buf]
(let [break-str (break)]
(->> (vim.api.nvim_buf_get_lines buf 0 -1 false)
(core.kv-pairs)
(core.filter
(fn [[_n s]]
(= s break-str)))
(core.map core.first))))
(fn set-win-opts! [win]
(tset vim.wo win :wrap (if (config.get-in [:log :wrap]) true false))
(tset vim.wo win :foldmethod :marker)
(tset vim.wo win :foldmarker (.. (config.get-in [:log :fold :marker :start])
","
(config.get-in [:log :fold :marker :end])))
(tset vim.wo win :foldlevel 0))
(fn in-box? [box pos]
(and (>= pos.x box.x1) (<= pos.x box.x2)
(>= pos.y box.y1) (<= pos.y box.y2)))
(fn flip-anchor [anchor n]
(let [chars [(anchor:sub 1 1)
(anchor:sub 2)]
flip {:N :S
:S :N
:E :W
:W :E}]
(str.join (core.update chars n #(core.get flip $1)))))
(fn pad-box [box padding]
(-> box
(core.update :x1 #(- $1 padding.x))
(core.update :y1 #(- $1 padding.y))
(core.update :x2 #(+ $1 padding.x))
(core.update :y2 #(+ $1 padding.y))))
(fn hud-window-pos [anchor size rec?]
(let [north 0 west 0
south (- (editor.height) 2)
east (editor.width)
padding-percent (config.get-in [:log :hud :overlap_padding])
pos (-> (if
(= :NE anchor) {:row north :col east
:box {:y1 north :x1 (- east size.width)
:y2 (+ north size.height) :x2 east}}
(= :SE anchor) {:row south :col east
:box {:y1 (- south size.height) :x1 (- east size.width)
:y2 south :x2 east}}
(= :SW anchor) {:row south :col west
:box {:y1 (- south size.height) :x1 west
:y2 south :x2 (+ west size.width)}}
(= :NW anchor) {:row north :col west
:box {:y1 north :x1 west
:y2 (+ north size.height) :x2 (+ west size.width)}}
(do
(vim.notify "g:conjure#log#hud#anchor must be one of: NE, SE, SW, NW"
vim.log.levels.ERROR)
(hud-window-pos :NE size)))
(core.assoc :anchor anchor))]
(if (and (not rec?)
(in-box?
(pad-box
pos.box
{:x (editor.percent-width padding-percent)
:y (editor.percent-height padding-percent)})
{:x (editor.cursor-left)
:y (editor.cursor-top)}))
(hud-window-pos
(flip-anchor anchor (if (> size.width size.height) 1 2))
size true)
pos)))
(fn current-window-floating? []
(= :number (type (core.get (vim.api.nvim_win_get_config 0) :zindex))))
(local low-priority-streak-threshold 5)
(fn handle-low-priority-spam! [low-priority?]
;; When we see a bunch of low-priority? messages opening the HUD repeatedly
;; we display a bit of help _once_ that can prevent this spam in the future
;; for the user.
(when (not (core.get-in M.state [:hud :low-priority-spam :help-displayed?]))
(if low-priority?
(core.update-in M.state [:hud :low-priority-spam :streak] core.inc)
(core.assoc-in M.state [:hud :low-priority-spam :streak] 0))
(when (> (core.get-in M.state [:hud :low-priority-spam :streak]) low-priority-streak-threshold)
(let [pref (client.get :comment-prefix)]
(client.schedule
(. (require :conjure.log) :append)
[(.. pref "Is the HUD popping up too much and annoying you in this project?")
(.. pref "Set this option to suppress this kind of output for this session.")
(.. pref " :let g:conjure#log#hud#ignore_low_priority = v:true")]
{:break? true}))
(core.assoc-in M.state [:hud :low-priority-spam :help-displayed?] true))))
(hook.define
:display-hud
(fn [opts]
(let [buf (upsert-buf)
last-break (core.last (break-lines buf))
line-count (vim.api.nvim_buf_line_count buf)
size {:width (editor.percent-width (config.get-in [:log :hud :width]))
:height (editor.percent-height (config.get-in [:log :hud :height]))}
pos (hud-window-pos (config.get-in [:log :hud :anchor]) size)
border (config.get-in [:log :hud :border])
win-opts
(core.merge
{:relative :editor
:row pos.row
:col pos.col
:anchor pos.anchor
:width size.width
:height size.height
:focusable false
:style :minimal
:zindex (config.get-in [:log :hud :zindex])
:border border})]
(when (and M.state.hud.id (not (vim.api.nvim_win_is_valid M.state.hud.id)))
(M.close-hud))
(if M.state.hud.id
(vim.api.nvim_win_set_buf M.state.hud.id buf)
(do
(handle-low-priority-spam! (core.get opts :low-priority?))
(set M.state.hud.id (vim.api.nvim_open_win buf false win-opts))
(set-win-opts! M.state.hud.id)))
(set M.state.hud.created-at-ms (vim.uv.now))
(if last-break
(do
(vim.api.nvim_win_set_cursor M.state.hud.id [1 0])
(vim.api.nvim_win_set_cursor M.state.hud.id
[(math.min
(+ last-break
(core.inc (math.floor (/ win-opts.height 2))))
line-count)
0]))
(vim.api.nvim_win_set_cursor M.state.hud.id [line-count 0])))))
(fn M.mark-eval-seen! []
(set M.state.hud.eval-seen? true))
(fn display-hud [opts]
(when (and (config.get-in [:log :hud :enabled])
(or (config.get-in [:log :hud :open_before_first_eval])
M.state.hud.eval-seen?)
;; Don't display when the user is already doing something in a floating window.
(not (current-window-floating?))
;; Don't display low priority messages if configured.
(or (not (config.get-in [:log :hud :ignore_low_priority]))
(and (config.get-in [:log :hud :ignore_low_priority])
(not (core.get opts :low-priority?)))))
(M.clear-close-hud-passive-timer)
(hook.exec :display-hud opts)))
(fn M.show-hud []
(M.clear-close-hud-passive-timer)
(hook.exec :display-hud {}))
(fn win-visible? [win]
(= (vim.fn.tabpagenr)
(core.first (vim.fn.win_id2tabwin win))))
(fn with-buf-wins [buf f]
(core.run!
(fn [win]
(when (= buf (vim.api.nvim_win_get_buf win))
(f win)))
(vim.api.nvim_list_wins)))
(fn win-botline [win]
(-> win
(vim.fn.getwininfo)
(core.first)
(core.get :botline)))
(fn trim [buf]
(let [line-count (vim.api.nvim_buf_line_count buf)]
(when (> line-count (config.get-in [:log :trim :at]))
(let [target-line-count (- line-count (config.get-in [:log :trim :to]))
break-line
(core.some
(fn [line]
(when (>= line target-line-count)
line))
(break-lines buf))]
(when break-line
(vim.api.nvim_buf_set_lines buf 0 break-line false [])
;; This hack keeps all log window view ports correct after trim.
;; Without it the text moves off screen in the HUD.
(with-buf-wins
buf
(fn [win]
(let [[row col] (vim.api.nvim_win_get_cursor win)]
(vim.api.nvim_win_set_cursor win [1 0])
(vim.api.nvim_win_set_cursor win [row col])))))))))
(fn M.last-line [buf extra-offset]
(core.first
(vim.api.nvim_buf_get_lines
(or buf (upsert-buf))
(+ -2 (or extra-offset 0)) -1 false)))
(set M.cursor-scroll-position->command
{:top "normal zt"
:center "normal zz"
:bottom "normal zb"
:last "normal G"
:none nil})
(fn M.jump-to-latest []
(M.close-hud)
(let [buf (upsert-buf)
last-eval-start (vim.api.nvim_buf_get_extmark_by_id
buf M.state.jump-to-latest.ns
M.state.jump-to-latest.mark {})]
(with-buf-wins
buf
(fn [win]
(pcall #(vim.api.nvim_win_set_cursor win last-eval-start))
(let [cmd (core.get
M.cursor-scroll-position->command
(config.get-in [:log :jump_to_latest :cursor_scroll_position]))]
(when cmd
(vim.api.nvim_win_call win (fn [] (vim.cmd cmd)))))))))
(fn M.immediate-append [lines opts]
(let [line-count (core.count lines)]
(when (> line-count 0)
(var visible-scrolling-log? false)
(var visible-log? false)
(let [buf (upsert-buf)
join-first? (core.get opts :join-first?)
;; A failsafe for newlines in lines. They _should_ be split up by
;; the calling code but this means we at least print the line
;; rather than throwing an error.
;; We also ensure every value _is_ a string. If we have a nil in
;; here it will at least be the right type for the gsub.
lines (core.map
(fn [line]
(string.gsub (tostring line) "\n" "↵"))
lines)
lines (if (<= line-count
(config.get-in [:log :strip_ansi_escape_sequences_line_limit]))
(core.map text.strip-ansi-escape-sequences lines)
lines)
comment-prefix (client.get :comment-prefix)
;; Optionally insert fold markers.
;; When we're not doing a "break" (seperator).
;; Not joining with the previous line.
;; Folding is enabled and we crossed the line count threshold.
fold-marker-end (str.join [comment-prefix (config.get-in [:log :fold :marker :end])])
lines (if (and (not (core.get opts :break?))
(not join-first?)
(config.get-in [:log :fold :enabled])
(>= (core.count lines) (config.get-in [:log :fold :lines])))
(core.concat
[(str.join [comment-prefix
(config.get-in [:log :fold :marker :start])
" "
(text.left-sample
(str.join "\n" lines)
(editor.percent-width
(config.get-in [:preview :sample_limit])))])]
lines
[fold-marker-end])
lines)
;; When the last line in the buffer is a closing fold marker...
;; It means join-first? should account for it so it joins _inside_
;; the fold block by including the fold end line in the replacement.
last-fold? (= fold-marker-end (M.last-line buf))
;; Insert break comments or join continuing lines if required.
lines (if
(core.get opts :break?)
(core.concat
[(break)]
(when (client.multiple-states?)
[(state-key-header)])
lines)
join-first?
(core.concat
(if last-fold?
[(.. (M.last-line buf -1)
(core.first lines))
fold-marker-end]
[(.. (M.last-line buf) (core.first lines))])
(core.rest lines))
lines)
old-lines (vim.api.nvim_buf_line_count buf)]
(let [(ok? err)
(pcall
(fn []
(vim.api.nvim_buf_set_lines
buf
(if
(buffer.empty? buf) 0
;; Replace one extra line if joining across fold markers.
join-first? (if last-fold? -3 -2)
-1)
-1 false lines)))]
(when (not ok?)
(error (.. "Conjure failed to append to log: " err "\n"
"Offending lines: " (core.pr-str lines)))))
(let [new-lines (vim.api.nvim_buf_line_count buf)
jump-to-latest? (config.get-in [:log :jump_to_latest :enabled])]
;; This mark is used when jumping to the latest log entry.
(vim.api.nvim_buf_set_extmark
buf M.state.jump-to-latest.ns
(if join-first?
old-lines
(core.inc old-lines)) 0
{:id M.state.jump-to-latest.mark})
(with-buf-wins
buf
(fn [win]
(set visible-scrolling-log?
(and (not= win M.state.hud.id)
(win-visible? win)
(or jump-to-latest?
(>= (win-botline win) old-lines))))
(set visible-log?
(and (not= win M.state.hud.id)
(win-visible? win)))
(let [[row _] (vim.api.nvim_win_get_cursor win)]
(if jump-to-latest?
(M.jump-to-latest)
(= row old-lines)
(vim.api.nvim_win_set_cursor win [new-lines 0]))))))
(let [open-when (config.get-in [:log :hud :open_when])]
(if (and (not (core.get opts :suppress-hud?))
(or (and (= :last-log-line-not-visible open-when)
(not visible-scrolling-log?))
(and (= :log-win-not-visible open-when)
(not visible-log?))))
(display-hud opts)
(trim buf)))))))
(fn M.flush []
(each [filetype buffer (pairs M.state.buffers)]
(let [batched-lines []
batched-opts {:clientsuppress-hud? true
:low-priority? true}]
(each [_ [lines opts] (ipairs buffer)]
(when (not (core.empty? lines))
(each [_ line (ipairs lines)]
(table.insert batched-lines line)))
;; Flush eagerly when we see this.
(when (core.get opts :break?)
(tset batched-opts :break? true))
;; Flush eagerly when we see this.
(when (core.get opts :join-first?)
(tset batched-opts :join-first? true))
;; Only if ALL are true.
(when (not (core.get opts :suppress-hud?))
(tset batched-opts :suppress-hud? nil))
;; Only if ALL are true.
(when (not (core.get opts :low-priority?))
(tset batched-opts :low-priority? nil)))
(when (not (core.empty? batched-lines))
(client.with-filetype filetype M.immediate-append batched-lines batched-opts)))
(tset M.state.buffers filetype nil)))
(fn M.setup-auto-flush []
(timer.interval (config.get-in [:log :auto_flush_interval_ms]) M.flush))
(fn M.append [lines opts]
(let [eager? (or (core.get opts :break?) (core.get opts :join-first?))]
(when eager?
(M.flush))
(let [{: filetype} (client.current-client-module-name)
buffer (or (. M.state.buffers filetype) [])]
(table.insert buffer [lines opts])
(tset M.state.buffers filetype buffer))
(when eager?
(M.flush))))
(fn create-win [cmd]
(set M.state.last-open-cmd cmd)
(let [buf (upsert-buf)]
(vim.cmd (string.format "keepalt %s %s %s"
(if (config.get-in [:log :botright]) "botright" "")
cmd
(buffer.resolve (log-buf-name))))
(vim.api.nvim_win_set_cursor 0 [(vim.api.nvim_buf_line_count buf) 0])
(set-win-opts! 0)
(buffer.unlist buf)))
(fn M.split []
(create-win :split)
(let [height (config.get-in [:log :split :height])]
(when height
(vim.api.nvim_win_set_height 0 (editor.percent-height height)))))
(fn M.vsplit []
(create-win :vsplit)
(let [width (config.get-in [:log :split :width])]
(when width
(vim.api.nvim_win_set_width 0 (editor.percent-width width)))))
(fn M.tab []
(create-win :tabnew))
(fn M.buf []
(create-win :buf))
(fn find-windows []
(let [buf (upsert-buf)]
(core.filter (fn [win] (and (not= M.state.hud.id win)
(= buf (vim.api.nvim_win_get_buf win))))
(vim.api.nvim_tabpage_list_wins 0))))
(fn close [windows]
(core.run! #(vim.api.nvim_win_close $1 true) windows))
(fn M.close-visible []
(M.close-hud)
(close (find-windows)))
(fn M.toggle []
(let [windows (find-windows)]
(if (core.empty? windows)
(when (or (= M.state.last-open-cmd :split)
(= M.state.last-open-cmd :vsplit))
(create-win M.state.last-open-cmd))
(M.close-visible))))
(fn M.dbg [desc ...]
(when (config.get-in [:debug])
(M.append
(core.concat
[(.. (client.get :comment-prefix) "debug: " desc)]
(text.split-lines (core.pr-str ...)))))
...)
(fn M.reset-soft []
(on-new-log-buf (upsert-buf)))
(fn M.reset-hard []
(vim.api.nvim_buf_delete (upsert-buf) {:force true}))
M