` | Additional environment variables for the task |
+| >disallow_prompt | `nil\|boolean` | When true, if any required parameters are missing return an error instead of prompting the user for them |
+| >on_build | `nil\|fun(task_defn: overseer.TaskDefinition, util: overseer.TaskUtil)` | callback that is called after the task definition is built but before the task is created. |
+| callback | `nil\|fun(task: overseer.Task\|nil, err: string\|nil)` | |
**Examples:**
```lua
-- Run the task named "make all"
-- equivalent to :OverseerRun make\ all
-overseer.run_template({name = "make all"})
+overseer.run_task({name = "make all"})
-- Run the default "build" task
-- equivalent to :OverseerRun BUILD
-overseer.run_template({tags = {overseer.TAG.BUILD}})
+overseer.run_task({tags = {overseer.TAG.BUILD}})
-- Run the task named "serve" with some default parameters
-overseer.run_template({name = "serve", params = {port = 8080}})
+overseer.run_task({name = "serve", params = {port = 8080}})
-- Create a task but do not start it
-overseer.run_template({name = "make", autostart = false}, function(task)
+overseer.run_task({name = "make", autostart = false}, function(task)
-- do something with the task
end)
-- Run a task and immediately open the floating window
-overseer.run_template({name = "make"}, function(task)
+overseer.run_task({name = "make"}, function(task)
if task then
overseer.run_action(task, 'open float')
end
end)
--- Run a task and always show the parameter prompt
-overseer.run_template({name = "npm watch", prompt = "always"})
```
### preload_task_cache(opts, cb)
`preload_task_cache(opts, cb)` \
-Preload templates for run_template
+Preload templates for run_task
-| Param | Type | Desc |
-| ----- | ------------- | ---------------------------------- |
-| opts | `nil\|table` | |
-| >dir | `string` | |
-| >ft | `nil\|string` | |
-| cb | `nil\|fun()` | Called when preloading is complete |
+| Param | Type | Desc |
+| --------- | ---------------------------- | ---------------------------------- |
+| opts | `nil\|overseer.SearchParams` | |
+| >filetype | `nil\|string` | |
+| >tags | `nil\|string[]` | |
+| >dir | `string` | |
+| cb | `nil\|fun()` | Called when preloading is complete |
**Note:**
@@ -554,13 +403,14 @@ vim.api.nvim_create_autocmd({"VimEnter", "DirChanged"}, {
### clear_task_cache(opts)
`clear_task_cache(opts)` \
-Clear cached templates for run_template
+Clear cached templates for run_task
-| Param | Type | Desc |
-| ----- | ------------- | ---- |
-| opts | `nil\|table` | |
-| >dir | `string` | |
-| >ft | `nil\|string` | |
+| Param | Type | Desc |
+| --------- | ---------------------------- | ---- |
+| opts | `nil\|overseer.SearchParams` | |
+| >filetype | `nil\|string` | |
+| >tags | `nil\|string[]` | |
+| >dir | `string` | |
### run_action(task, name)
@@ -570,73 +420,19 @@ Run an action on a task
| Param | Type | Desc |
| ----- | --------------- | -------------------------------------------------- |
| task | `overseer.Task` | |
-| name | `string\|nil` | Name of action. When omitted, prompt user to pick. |
-
-### wrap_template(base, override, default_params)
-
-`wrap_template(base, override, default_params): overseer.TemplateFileDefinition` \
-Create a new template by overriding fields on another
-
-| Param | Type | Desc |
-| -------------- | -------------------------------------------------- | ----------------------------------------------------- |
-| base | `overseer.TemplateFileDefinition` | The base template definition to wrap |
-| >module | `nil\|string` | The name of the module this was loaded from |
-| >aliases | `nil\|string[]` | |
-| >desc | `nil\|string` | |
-| >tags | `nil\|string[]` | |
-| >params | `nil\|overseer.Params\|fun(): overseer.Params` | |
-| >priority | `nil\|number` | |
-| >condition | `nil\|overseer.SearchCondition` | |
-| >>filetype | `nil\|string\|string[]` | |
-| >>dir | `nil\|string\|string[]` | |
-| >>callback | `nil\|fun(search: overseer.SearchParams): boolean` | , nil|string |
-| >builder | `fun(params: table): overseer.TaskDefinition` | |
-| >hide | `nil\|boolean` | Hide from the template list |
-| override | `nil\|table` | Override any fields on the base |
-| default_params | `nil\|table` | Provide default values for any parameters on the base |
-
-**Note:**
-
-This is typically used for a TemplateProvider, to define the task a single time and generate
-multiple templates based on the available args.
-
-
-**Examples:**
-```lua
-local tmpl = {
- params = {
- args = { type = 'list', delimiter = ' ' }
- },
- builder = function(params)
- return {
- cmd = { 'make' },
- args = params.args,
- }
-}
-local template_provider = {
- name = "Some provider",
- generator = function(opts, cb)
- cb({
- overseer.wrap_template(tmpl, nil, { args = { 'all' } }),
- overseer.wrap_template(tmpl, {name = 'make clean'}, { args = { 'clean' } }),
- })
- end
-}
-```
+| name | `nil\|string` | Name of action. When omitted, prompt user to pick. |
### add_template_hook(opts, hook)
`add_template_hook(opts, hook)` \
Add a hook that runs on a TaskDefinition before the task is created
-| Param | Type | Desc |
-| --------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------- |
-| opts | `nil\|overseer.HookOptions` | When nil, run the hook on all templates |
-| >name | `nil\|string` | Only run if the template name matches this pattern (using string.match) |
-| >module | `nil\|string` | Only run if the template module matches this pattern (using string.match) |
-| >filetype | `nil\|string\|string[]` | Only run if the current file is one of these filetypes |
-| >dir | `nil\|string\|string[]` | Only run if inside one of these directories |
-| hook | `fun(task_defn: overseer.TaskDefinition, util: overseer.TaskUtil)` | |
+| Param | Type | Desc |
+| ------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------- |
+| opts | `nil\|overseer.HookOptions` | When nil, run the hook on all templates |
+| >module | `nil\|string` | Only run if the template module matches this pattern (using string.match) |
+| >name | `nil\|string` | Only run if the template name matches this pattern (using string.match) |
+| hook | `fun(task_defn: overseer.TaskDefinition, util: overseer.TaskUtil)` | |
**Examples:**
```lua
@@ -661,12 +457,12 @@ end)
`remove_template_hook(opts, hook)` \
Remove a hook that was added with add_template_hook
-| Param | Type | Desc |
-| ------- | ------------------------------------------------------------------ | ----------------------------- |
-| opts | `nil\|overseer.HookOptions` | Same as for add_template_hook |
-| >module | `nil\|string` | |
-| >name | `nil\|string` | |
-| hook | `fun(task_defn: overseer.TaskDefinition, util: overseer.TaskUtil)` | |
+| Param | Type | Desc |
+| ------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------- |
+| opts | `nil\|overseer.HookOptions` | Same as for add_template_hook |
+| >module | `nil\|string` | Only run if the template module matches this pattern (using string.match) |
+| >name | `nil\|string` | Only run if the template name matches this pattern (using string.match) |
+| hook | `fun(task_defn: overseer.TaskDefinition, util: overseer.TaskUtil)` | |
**Examples:**
```lua
@@ -700,65 +496,325 @@ overseer.register_template({
})
```
-### load_template(name)
+### register_alias(name, components, override)
-`load_template(name)` \
-Load a template definition from its module location
+`register_alias(name, components, override)` \
+Register a new component alias.
-| Param | Type | Desc |
-| ----- | -------- | ---- |
-| name | `string` | |
+| Param | Type | Desc |
+| ---------- | ----------------------- | --------------------------------------------------------- |
+| name | `string` | |
+| components | `overseer.Serialized[]` | |
+| override | `nil\|boolean` | When true, override any existing alias with the same name |
+
+**Note:**
+
+This is intended to be used by plugin authors that wish to build on top of overseer. They do not
+have control over the call to overseer.setup(), so this provides an alternative method of
+setting a component alias that they can then use when creating tasks.
+
+
+**Examples:**
+```lua
+require("overseer").register_alias("my_plugin", { "default", "on_output_quickfix" })
+```
+
+### create_task_output_view(winid, opts)
+
+`create_task_output_view(winid, opts)` \
+Set a window to display the output of a dynamically-chosen task
+
+| Param | Type | Desc |
+| -------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
+| winid | `nil\|integer` | The window to use for displaying the task output |
+| opts | `nil\|overseer.TaskViewOpts` | |
+| >select | `nil\|fun(self: overseer.TaskView, tasks: overseer.Task[], task_under_cursor?: overseer.Task): nil\|overseer.Task` | Select which task in the task list to display the output of |
+| >close_on_list_close | `nil\|boolean` | Close the window when the task list is closed |
+| >list_task_opts | `nil\|overseer.ListTaskOpts` | Passed to list_tasks() to get the list of tasks to pass to the select() function |
+| >>unique | `nil\|boolean` | Deduplicates non-running tasks by name |
+| >>status | `nil\|overseer.Status\|overseer.Status[]` | Only list tasks with this status or statuses |
+| >>include_ephemeral | `nil\|boolean` | Include ephemeral tasks |
+| >>wrapped | `nil\|boolean` | Include tasks that were created by the jobstart/vim.system wrappers |
+| >>filter | `nil\|fun(task: overseer.Task): boolean` | Only include tasks where this function returns true |
+| >>sort | `nil\|fun(a: overseer.Task, b: overseer.Task): boolean` | Function that sorts tasks |
**Examples:**
```lua
--- This will load the template in lua/overseer/template/mytask.lua
-overseer.load_template('mytask')
+-- Always show the output from the most recent Neotest task in this window.
+-- Close it automatically when all test tasks are disposed.
+overseer.create_task_output_view(0, {
+ select = function(self, tasks, task_under_cursor)
+ for _, task in ipairs(tasks) do
+ if task.metadata.neotest_group_id then
+ return task
+ end
+ end
+ self:dispose()
+ end,
+})
```
-### debug_parser()
-`debug_parser()` \
-Open a tab with windows laid out for debugging a parser
+
+
+### overseer.Task
-### register_alias(name, components)
+| Field | Type | Desc |
+| ---------- | ---------------------------- | ------------------------------------------------------------------------------------------------------ |
+| id | `integer` | Unique ID for this task |
+| result | `nil\|table` | For successful tasks, arbitrary key-value mapping of data produced by components |
+| metadata | `table` | Arbitrary key-value mapping passed by the user during construction |
+| status | `overseer.Status` | Current task status |
+| cmd | `string\|string[]` | Command to run. If it's a string it is run in the shell |
+| cwd | `string` | Working directory the task is run in |
+| env | `nil\|table` | Additional environment variables for the task |
+| name | `string` | Name of the task |
+| ephemeral | `boolean` | Indicates that this task was generated indirectly (e.g. with run_after) |
+| source | `nil\|overseer.Caller` | If this task was created by wrapping jobstart/vim.system, this contains information about the callsite |
+| exit_code | `nil\|integer` | Exit code of the task process |
+| parent_id | `nil\|integer` | ID of parent task. Used only to visually group tasks in the task list |
+| time_start | `nil\|integer` | Timestamp when the task was started (os.time()) |
+| time_end | `nil\|integer` | Timestamp when the task ended (os.time()) |
+
+
+#### Task:serialize()
+
+`Task:serialize(): overseer.TaskDefinition` \
+Returns the arguments require to create a clone of this task when passed to overseer.new_task
+
+
+#### Task:clone()
+
+`Task:clone(): overseer.Task` \
+Create a deep copy of this task
-`register_alias(name, components)` \
-Register a new component alias.
+
+#### Task:add_component(comp)
+
+`Task:add_component(comp)` \
+Add a component, no-op if it already exists
+
+| Param | Type | Desc |
+| ----- | --------------------- | ---- |
+| comp | `overseer.Serialized` | |
+
+#### Task:add_components(components)
+
+`Task:add_components(components)` \
+Add components, skipping any that already exist
+
+| Param | Type | Desc |
+| ---------- | ----------------------- | ---- |
+| components | `overseer.Serialized[]` | |
+
+#### Task:set_component(comp)
+
+`Task:set_component(comp)` \
+Add component, overwriting any existing
+
+| Param | Type | Desc |
+| ----- | --------------------- | ---- |
+| comp | `overseer.Serialized` | |
+
+#### Task:set_components(components)
+
+`Task:set_components(components)` \
+Add components, overwriting any existing
| Param | Type | Desc |
| ---------- | ----------------------- | ---- |
-| name | `string` | |
| components | `overseer.Serialized[]` | |
+#### Task:get_component(name)
+
+`Task:get_component(name): nil|overseer.Component`
+
+| Param | Type | Desc |
+| ----- | -------- | ---- |
+| name | `string` | |
+
+#### Task:remove_component(name)
+
+`Task:remove_component(name): nil|overseer.Component`
+
+| Param | Type | Desc |
+| ----- | -------- | ---- |
+| name | `string` | |
+
+#### Task:remove_components(names)
+
+`Task:remove_components(names): overseer.Component[]`
+
+| Param | Type | Desc |
+| ----- | ---------- | ---- |
+| names | `string[]` | |
+
+#### Task:has_component(name)
+
+`Task:has_component(name): boolean`
+
+| Param | Type | Desc |
+| ----- | -------- | ---- |
+| name | `string` | |
+
+#### Task:subscribe(event, callback)
+
+`Task:subscribe(event, callback)` \
+Subscribe to events on this task
+
+| Param | Type | Desc |
+| -------- | -------------------------------------------------- | -------------------------------------------------------- |
+| event | `string` | |
+| callback | `fun(task: overseer.Task, ...: any): nil\|boolean` | Callback can return a truthy value to unsubscribe itself |
+
**Note:**
-This is intended to be used by plugin authors that wish to build on top of overseer. They do not
-have control over the call to overseer.setup(), so this provides an alternative method of
-setting a component alias that they can then use when creating tasks.
+Listeners cannot be serialized, so will not be saved when saving task
+to disk and will not be copied when cloning the task.
-**Examples:**
-```lua
-require("overseer").register_alias("my_plugin", { "default", "on_output_quickfix" })
-```
+#### Task:unsubscribe(event, callback)
+`Task:unsubscribe(event, callback)` \
+Unsubscribe from an event that was previously subscribed to
-
+| Param | Type | Desc |
+| -------- | ------------------------------------ | ---- |
+| event | `string` | |
+| callback | `fun(task: overseer.Task, ...: any)` | |
+
+#### Task:is_pending()
+
+`Task:is_pending(): boolean` \
+Returns true if the task is PENDING
+
+
+#### Task:is_running()
+
+`Task:is_running(): boolean` \
+Returns true if the task is RUNNING
+
+
+#### Task:is_complete()
+
+`Task:is_complete(): boolean` \
+Returns true if the task is complete (not PENDING or RUNNING)
+
+
+#### Task:is_disposed()
+
+`Task:is_disposed(): boolean` \
+Returns true if the task is DISPOSED
+
+
+#### Task:get_bufnr()
+
+`Task:get_bufnr(): number|nil` \
+Get the buffer containing the task output. Will be nil if task is PENDING.
+
+
+#### Task:open_output(direction)
+
+`Task:open_output(direction)` \
+Open the task output in a window
+
+| Param | Type | Desc |
+| --------- | ----------------------------------------------- | ---- |
+| direction | `nil\|"float"\|"tab"\|"vertical"\|"horizontal"` | |
+
+**Note:**
+
+You can also use get_bufnr() to get the buffer and open it however you like.
+
+
+#### Task:broadcast(name)
+
+`Task:broadcast(name)` \
+Dispatch an event to all other tasks
+
+| Param | Type | Desc |
+| ----- | -------- | ---- |
+| name | `string` | |
+
+#### Task:dispatch(name)
+
+`Task:dispatch(name): any[]` \
+Dispatch an event to all components
+
+| Param | Type | Desc |
+| ----- | -------- | ---- |
+| name | `string` | |
+
+#### Task:inc_reference()
+
+`Task:inc_reference()` \
+Increment the refcount for this Task, preventing it from being disposed (unless force=true)
+
+
+#### Task:dec_reference()
+
+`Task:dec_reference()` \
+Decrement the refcount for this Task
+
+
+#### Task:dispose(force)
+
+`Task:dispose(force): boolean` \
+Cleans up resources, removes from task list, and deletes buffer.
+
+| Param | Type | Desc |
+| ----- | -------------- | ------------------------------------------------------------------------------ |
+| force | `nil\|boolean` | When true, will dispose even with a nonzero refcount or when buffer is visible |
+
+Returns:
+
+| Type | Desc |
+| ------- | ---------------------------------- |
+| boolean | disposed True if task was disposed |
+
+#### Task:restart(force_stop)
+
+`Task:restart(force_stop): boolean` \
+Reset and re-run the task
+
+| Param | Type | Desc |
+| ---------- | -------------- | --------------------------------------------------------- |
+| force_stop | `nil\|boolean` | If true, restart the Task even if it is currently running |
+
+#### Task:start()
+
+`Task:start()` \
+Start a pending task
+
+
+#### Task:stop()
+
+`Task:stop(): boolean` \
+Stop a running task
+
+
+Returns:
+
+| Type | Desc |
+| ------- | ------------------------------------ |
+| boolean | stopped True if the task was stopped |
+
+
+
## Components
- [dependencies](components.md#dependencies)
-- [display_duration](components.md#display_duration)
- [on_complete_dispose](components.md#on_complete_dispose)
- [on_complete_notify](components.md#on_complete_notify)
- [on_complete_restart](components.md#on_complete_restart)
- [on_exit_set_status](components.md#on_exit_set_status)
+- [on_output_notify](components.md#on_output_notify)
- [on_output_parse](components.md#on_output_parse)
- [on_output_quickfix](components.md#on_output_quickfix)
-- [on_output_summarize](components.md#on_output_summarize)
- [on_output_write_file](components.md#on_output_write_file)
- [on_result_diagnostics](components.md#on_result_diagnostics)
- [on_result_diagnostics_quickfix](components.md#on_result_diagnostics_quickfix)
@@ -778,36 +834,11 @@ require("overseer").register_alias("my_plugin", { "default", "on_output_quickfix
- [jobstart(opts)](strategies.md#jobstartopts)
- [orchestrator(opts)](strategies.md#orchestratoropts)
-- [terminal()](strategies.md#terminal)
+- [system(opts)](strategies.md#systemopts)
- [test()](strategies.md#test)
-- [toggleterm(opts)](strategies.md#toggletermopts)
-## Parsers
-
-
-
- - [always](parsers.md#always)
- - [append](parsers.md#append)
- - [dispatch](parsers.md#dispatch)
- - [ensure](parsers.md#ensure)
- - [extract](parsers.md#extract)
- - [extract_efm](parsers.md#extract_efm)
- - [extract_json](parsers.md#extract_json)
- - [extract_multiline](parsers.md#extract_multiline)
- - [extract_nested](parsers.md#extract_nested)
- - [invert](parsers.md#invert)
- - [loop](parsers.md#loop)
- - [parallel](parsers.md#parallel)
- - [sequence](parsers.md#sequence)
- - [set_defaults](parsers.md#set_defaults)
- - [skip_lines](parsers.md#skip_lines)
- - [skip_until](parsers.md#skip_until)
- - [test](parsers.md#test)
-
-
-
## Parameters
Parameters are a schema-defined set of options. They are used by both [components](explanation.md#components) and [templates](explanation.md#templates) to expose customization options.
@@ -821,7 +852,7 @@ local params = {
desc = "A detailed description",
order = 1, -- determines order of parameters in the UI
validate = function(value)
- return true,
+ return true
end,
optional = true,
default = "foobar",
diff --git a/doc/rendering.md b/doc/rendering.md
new file mode 100644
index 00000000..0bf35169
--- /dev/null
+++ b/doc/rendering.md
@@ -0,0 +1,394 @@
+# Rendering
+
+These are some built-in task formatting functions and a library of useful pieces that you can use to build your own formats.
+
+
+
+- [format_standard(task)](#format_standardtask)
+- [format_compact(task)](#format_compacttask)
+- [format_verbose(task)](#format_verbosetask)
+- [status(task)](#statustask)
+- [name(task)](#nametask)
+- [status_and_name(task)](#status_and_nametask)
+- [cmd(task)](#cmdtask)
+- [result_lines(task, opts)](#result_linestask-opts)
+- [duration(task, opts)](#durationtask-opts)
+- [time_since_completed(task, opts)](#time_since_completedtask-opts)
+- [output_lines(task, opts)](#output_linestask-opts)
+- [source_lines(task, opts)](#source_linestask-opts)
+- [join(a, b, sep)](#joina-b-sep)
+- [remove_empty_lines(lines)](#remove_empty_lineslines)
+
+
+
+
+
+## format_standard(task)
+
+`format_standard(task): overseer.TextChunk[]` \
+The default format for tasks in the task list
+
+| Param | Type | Desc |
+| ----- | --------------- | ---- |
+| task | `overseer.Task` | |
+
+Returns:
+
+| Type | Desc |
+| -------------------- | ---- |
+| overseer.TextChunk[] | [] |
+
+**Examples:**
+```lua
+require("overseer").setup({
+ task_list = {
+ render = function(task)
+ return require("overseer.render").format_standard(task)
+ end,
+ },
+})
+```
+
+## format_compact(task)
+
+`format_compact(task): overseer.TextChunk[]` \
+A more compact format for tasks
+
+| Param | Type | Desc |
+| ----- | --------------- | ---- |
+| task | `overseer.Task` | |
+
+Returns:
+
+| Type | Desc |
+| -------------------- | ---- |
+| overseer.TextChunk[] | [] |
+
+**Examples:**
+```lua
+require("overseer").setup({
+ task_list = {
+ render = function(task)
+ return require("overseer.render").format_compact(task)
+ end,
+ },
+})
+```
+
+## format_verbose(task)
+
+`format_verbose(task): overseer.TextChunk[]` \
+A more verbose format for tasks
+
+| Param | Type | Desc |
+| ----- | --------------- | ---- |
+| task | `overseer.Task` | |
+
+Returns:
+
+| Type | Desc |
+| -------------------- | ---- |
+| overseer.TextChunk[] | [] |
+
+**Examples:**
+```lua
+require("overseer").setup({
+ task_list = {
+ render = function(task)
+ return require("overseer.render").format_verbose(task)
+ end,
+ },
+})
+```
+
+## status(task)
+
+`status(task): overseer.TextChunk[]` \
+Text chunks that display the status of a task
+
+| Param | Type | Desc |
+| ----- | --------------- | ---- |
+| task | `overseer.Task` | |
+
+**Examples:**
+```lua
+require("overseer").setup({
+ task_list = {
+ render = function(task)
+ local render = require("overseer.render")
+ return {
+ render.status(task),
+ { { task.name, "OverseerTask" } },
+ }
+ end,
+ },
+})
+```
+
+## name(task)
+
+`name(task): overseer.TextChunk[]` \
+Text chunks that display the name of a task
+
+| Param | Type | Desc |
+| ----- | --------------- | ---- |
+| task | `overseer.Task` | |
+
+**Examples:**
+```lua
+require("overseer").setup({
+ task_list = {
+ render = function(task)
+ local render = require("overseer.render")
+ return {
+ render.name(task),
+ }
+ end,
+ },
+})
+```
+
+## status_and_name(task)
+
+`status_and_name(task): overseer.TextChunk[]` \
+Text chunks that display the status and name of a task
+
+| Param | Type | Desc |
+| ----- | --------------- | ---- |
+| task | `overseer.Task` | |
+
+**Examples:**
+```lua
+require("overseer").setup({
+ task_list = {
+ render = function(task)
+ local render = require("overseer.render")
+ return {
+ render.status_and_name(task),
+ }
+ end,
+ },
+})
+```
+
+## cmd(task)
+
+`cmd(task): overseer.TextChunk[]` \
+Text chunks that display the command that was run
+
+| Param | Type | Desc |
+| ----- | --------------- | ---- |
+| task | `overseer.Task` | |
+
+**Examples:**
+```lua
+require("overseer").setup({
+ task_list = {
+ render = function(task)
+ local render = require("overseer.render")
+ return {
+ { { task.name, "OverseerTask" } },
+ render.cmd(task),
+ }
+ end,
+ },
+})
+```
+
+## result_lines(task, opts)
+
+`result_lines(task, opts): overseer.TextChunk[]` \
+Lines that display the result of a task
+
+| Param | Type | Desc |
+| ----- | -------------------------- | ---- |
+| task | `overseer.Task` | |
+| opts | `nil\|{oneline?: boolean}` | |
+
+Returns:
+
+| Type | Desc |
+| -------------------- | ---- |
+| overseer.TextChunk[] | [] |
+
+**Examples:**
+```lua
+require("overseer").setup({
+ task_list = {
+ render = function(task)
+ local render = require("overseer.render")
+ return vim.list_extend({
+ { { task.name, "OverseerTask" } },
+ }, render.result_lines(task, { oneline = false }))
+ end,
+ },
+})
+```
+
+## duration(task, opts)
+
+`duration(task, opts): overseer.TextChunk[]` \
+Text chunks that display how long a task has been running / ran for
+
+| Param | Type | Desc |
+| ----- | -------------------------- | ---- |
+| task | `overseer.Task` | |
+| opts | `nil\|{hl_group?: string}` | |
+
+**Examples:**
+```lua
+require("overseer").setup({
+ task_list = {
+ render = function(task)
+ local render = require("overseer.render")
+ return {
+ { { task.name, "OverseerTask" } },
+ render.duration(task),
+ }
+ end,
+ },
+})
+```
+
+## time_since_completed(task, opts)
+
+`time_since_completed(task, opts): overseer.TextChunk[]` \
+Text chunks that display the time since a task was completed
+
+| Param | Type | Desc |
+| ----- | -------------------------- | ---- |
+| task | `overseer.Task` | |
+| opts | `nil\|{hl_group?: string}` | |
+
+**Examples:**
+```lua
+require("overseer").setup({
+ task_list = {
+ render = function(task)
+ local render = require("overseer.render")
+ return {
+ { { task.name, "OverseerTask" } },
+ render.time_since_completed(task),
+ }
+ end,
+ },
+})
+```
+
+## output_lines(task, opts)
+
+`output_lines(task, opts): overseer.TextChunk[]` \
+Lines that display the last few lines of output from a task
+
+| Param | Type | Desc |
+| ----- | ----------------------------------------------------------------------- | ---- |
+| task | `overseer.Task` | |
+| opts | `nil\|{num_lines?: integer, prefix?: string, prefix_hl_group?: string}` | |
+
+Returns:
+
+| Type | Desc |
+| -------------------- | ---- |
+| overseer.TextChunk[] | [] |
+
+**Examples:**
+```lua
+require("overseer").setup({
+ task_list = {
+ render = function(task)
+ local render = require("overseer.render")
+ return vim.list_extend({
+ { { task.name, "OverseerTask" } },
+ }, render.output_lines(task, { num_lines = 3, prefix = "$ " }))
+ end,
+ },
+})
+```
+
+## source_lines(task, opts)
+
+`source_lines(task, opts): overseer.TextChunk[]` \
+Lines that display the source of a wrapped vim.system or vim.fn.jobstart task
+
+| Param | Type | Desc |
+| ----- | -------------------------- | ---- |
+| task | `overseer.Task` | |
+| opts | `nil\|{hl_group?: string}` | |
+
+Returns:
+
+| Type | Desc |
+| -------------------- | ---- |
+| overseer.TextChunk[] | [] |
+
+**Examples:**
+```lua
+require("overseer").setup({
+ task_list = {
+ render = function(task)
+ local render = require("overseer.render")
+ return vim.list_extend({
+ { { task.name, "OverseerTask" } },
+ }, render.source_lines(task, { num_lines = 3, prefix = "$ " }))
+ end,
+ },
+})
+```
+
+## join(a, b, sep)
+
+`join(a, b, sep): overseer.TextChunk[]` \
+Join two lists of text chunks together with a separator
+
+| Param | Type | Desc |
+| ----- | --------------------------------- | ---- |
+| a | `overseer.TextChunk[]` | |
+| b | `overseer.TextChunk[]` | |
+| sep | `nil\|string\|overseer.TextChunk` | |
+
+**Examples:**
+```lua
+require("overseer").setup({
+ task_list = {
+ render = function(task)
+ local render = require("overseer.render")
+ return {
+ render.join(render.status(task), render.name(task), ": "),
+ }
+ end,
+ },
+})
+```
+
+## remove_empty_lines(lines)
+
+`remove_empty_lines(lines): overseer.TextChunk[]` \
+Removes empty lines from a list of lines (each line is a list of text chunks)
+
+| Param | Type | Desc |
+| ----- | ---------------------- | ---- |
+| lines | `overseer.TextChunk[]` | [] |
+
+Returns:
+
+| Type | Desc |
+| -------------------- | ---- |
+| overseer.TextChunk[] | [] |
+
+**Examples:**
+```lua
+require("overseer").setup({
+ task_list = {
+ render = function(task)
+ local render = require("overseer.render")
+ local ret = vim.list_extend({
+ { { task.name, "OverseerTask" } },
+ }, render.output_lines(task, { num_lines = 3, prefix = "$ " }))
+ return render.remove_empty_lines(ret)
+ end,
+ },
+})
+```
+
+
+
diff --git a/doc/strategies.md b/doc/strategies.md
index abaabbee..cdf5daf8 100644
--- a/doc/strategies.md
+++ b/doc/strategies.md
@@ -6,9 +6,8 @@ The strategy is what controls how a task is actually run. The default, `terminal
- [jobstart(opts)](#jobstartopts)
- [orchestrator(opts)](#orchestratoropts)
-- [terminal()](#terminal)
+- [system(opts)](#systemopts)
- [test()](#test)
-- [toggleterm(opts)](#toggletermopts)
@@ -19,11 +18,12 @@ The strategy is what controls how a task is actually run. The default, `terminal
`jobstart(opts): overseer.Strategy` \
Run tasks using jobstart()
-| Param | Type | Desc |
-| ---------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
-| opts | `nil\|table` | |
-| >preserve_output | `boolean` | If true, don't clear the buffer when tasks restart |
-| >use_terminal | `boolean` | If false, use a normal non-terminal buffer to store the output. This may produce unwanted results if the task outputs terminal escape sequences. |
+| Param | Type | Desc |
+| ---------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
+| opts | `nil\|overseer.JobstartStrategyOpts` | |
+| >preserve_output | `nil\|boolean` | If true, don't clear the buffer when tasks restart |
+| >use_terminal | `nil\|boolean` | If false, use a normal non-terminal buffer to store the output. This may produce unwanted results if the task outputs terminal escape sequences. |
+| >wrap_opts | `nil\|table` | Opts that were passed to jobstart(). We should wrap them |
## orchestrator(opts)
@@ -53,11 +53,15 @@ overseer.new_task({
})
```
-## terminal()
+## system(opts)
-`terminal(): overseer.Strategy` \
-Run tasks using termopen()
+`system(opts): overseer.Strategy`
+| Param | Type | Desc |
+| ---------- | ------------------------------------ | ---------------------------------------------------------- |
+| opts | `nil\|overseer.SystemStrategyOpts` | |
+| >wrap_opts | `nil\|vim.SystemOpts` | Opts that were passed to vim.system(). We should wrap them |
+| >wrap_exit | `nil\|fun(out: vim.SystemCompleted)` | |
## test()
@@ -65,24 +69,5 @@ Run tasks using termopen()
Strategy used for unit testing
-## toggleterm(opts)
-
-`toggleterm(opts): overseer.Strategy` \
-Run tasks using the toggleterm plugin
-
-| Param | Type | Desc |
-| -------------- | ----------------------------------------------- | ------------------------------------------------------------------------ |
-| opts | `nil\|overseeer.ToggleTermStrategyOpts` | |
-| >use_shell | `nil\|boolean` | load user shell before running task |
-| >size | `nil\|number` | the size of the split if direction is vertical or horizontal |
-| >direction | `nil\|"vertical"\|"horizontal"\|"tab"\|"float"` | |
-| >highlights | `nil\|table` | map to a highlight group name and a table of it's values |
-| >auto_scroll | `nil\|boolean` | automatically scroll to the bottom on task output |
-| >close_on_exit | `nil\|boolean` | close the terminal and delete terminal buffer (if open) after task exits |
-| >quit_on_exit | `nil\|"never"\|"always"\|"success"` | close the terminal window (if open) after task exits |
-| >open_on_start | `nil\|boolean` | toggle open the terminal automatically when task starts |
-| >hidden | `nil\|boolean` | cannot be toggled with normal ToggleTerm commands |
-| >on_create | `nil\|fun(term: table)` | function to execute on terminal creation |
-
diff --git a/doc/third_party.md b/doc/third_party.md
index 52ed7b98..8ce802d6 100644
--- a/doc/third_party.md
+++ b/doc/third_party.md
@@ -6,7 +6,6 @@
- [Heirline](#heirline)
- [Neotest](#neotest)
- [DAP](#dap)
-- [ToggleTerm](#toggleterm)
- [Session managers](#session-managers)
- [resession.nvim](#resessionnvim)
- [Other session managers](#other-session-managers)
@@ -42,10 +41,8 @@ require("lualine").setup({
[overseer.STATUS.RUNNING] = "R:",
},
unique = false, -- Unique-ify non-running task count by name
- name = nil, -- List of task names to search for
- name_not = false, -- When true, invert the name search
status = nil, -- List of task statuses to display
- status_not = false, -- When true, invert the status search
+ filter = nil, -- Function to filter out tasks you don't wish to display
},
},
},
@@ -86,7 +83,7 @@ local Overseer = {
return package.loaded.overseer
end,
init = function(self)
- local tasks = require("overseer.task_list").list_tasks({ unique = true })
+ local tasks = require("overseer.task_list").list_tasks({ unique = true, include_ephemeral = true })
local tasks_by_status = require("overseer.util").tbl_group_by(tasks, "status")
self.tasks = tasks_by_status
end,
@@ -142,7 +139,6 @@ You can customize the default components of neotest tasks by setting the `defaul
require("overseer").setup({
component_aliases = {
default_neotest = {
- "on_output_summarize",
"on_exit_set_status",
"on_complete_notify",
"on_complete_dispose",
@@ -159,8 +155,8 @@ require("neotest").setup({
overseer = {
components = function(run_spec)
return {
- { "dependencies", task_names = {
- { "shell", cmd = "sleep 4" },
+ { "dependencies", tasks = {
+ { cmd = "sleep 4" },
} },
"default_neotest",
}
@@ -188,52 +184,6 @@ And enable the integration manually later, such as when `nvim-dap` is loaded:
require("overseer").enable_dap()
```
-## ToggleTerm
-
-If you use [toggleterm](https://github.com/akinsho/toggleterm.nvim), you can use the built-in "toggleterm" strategy to allow your tasks to be in a terminal buffer owned by toggleterm. You can use your existing toggleterm keybinds to pull up long-running tasks started with overseer. You can set it up with defaults using:
-
-```lua
-require("overseer").setup({
- strategy = "toggleterm",
-})
-```
-
-You can also configure the behavior a bit more:
-
-```lua
-require("overseer").setup({
- strategy = {
- "toggleterm",
- -- load your default shell before starting the task
- use_shell = false,
- -- overwrite the default toggleterm "direction" parameter
- direction = nil,
- -- overwrite the default toggleterm "highlights" parameter
- highlights = nil,
- -- overwrite the default toggleterm "auto_scroll" parameter
- auto_scroll = nil,
- -- have the toggleterm window close and delete the terminal buffer
- -- automatically after the task exits
- close_on_exit = false,
- -- have the toggleterm window close without deleting the terminal buffer
- -- automatically after the task exits
- -- can be "never, "success", or "always". "success" will close the window
- -- only if the exit code is 0.
- quit_on_exit = "never",
- -- open the toggleterm window when a task starts
- open_on_start = true,
- -- mirrors the toggleterm "hidden" parameter, and keeps the task from
- -- being rendered in the toggleable window
- hidden = false,
- -- command to run when the terminal is created. Combine with `use_shell`
- -- to run a terminal command before starting the task
- on_create = nil,
- },
-})
-```
-
-More documentation on this strategy can be found [here](strategies.md#toggletermopts).
-
## Session managers
### resession.nvim
@@ -254,41 +204,35 @@ The configuration options will be passed to [list_tasks](reference.md#list_tasks
### Other session managers
-For other session managers, task bundles should make it convenient to load/save tasks. These are exposed to the user with the commands `:OverseerSaveBundle` and `:OverseerLoadBundle`, but you can use the lua API directly for a nicer integration. You essentially just need to get the session name and add some hooks using your plugin's API to handle overseer tasks on session save/restore.
+For other session managers, the API allows you to list and serialize tasks. As long as your session
+manager has some way to store auxiliary data, you can use this to save and restore tasks.
For example, to integrate with [auto-session](https://github.com/rmagatti/auto-session)
```lua
--- Convert the cwd to a simple file name
-local function get_cwd_as_name()
- local dir = vim.fn.getcwd(0)
- return dir:gsub("[^A-Za-z0-9]", "_")
-end
-local overseer = require("overseer")
require("auto-session").setup({
pre_save_cmds = {
function()
- overseer.save_task_bundle(
- get_cwd_as_name(),
- -- Passing nil will use config.opts.save_task_opts. You can call list_tasks() explicitly and
- -- pass in the results if you want to save specific tasks.
- nil,
- { on_conflict = "overwrite" } -- Overwrite existing bundle, if any
- )
+ local tasks = require("overseer.task_list").list_tasks()
+ local cmds = {}
+ for _, task in ipairs(tasks) do
+ local json = vim.json.encode(task:serialize())
+ -- For some reason, vim.json.encode encodes / as \/.
+ json = string.gsub(json, "\\/", "/")
+ -- Escape single quotes so we can put this inside single quotes
+ json = string.gsub(json, "'", "\\'")
+ table.insert(cmds, string.format("lua require('overseer').new_task(vim.json.decode('%s')):start()", json))
+ end
+ return cmds
end,
},
-- Optionally get rid of all previous tasks when restoring a session
pre_restore_cmds = {
function()
- for _, task in ipairs(overseer.list_tasks({})) do
+ for _, task in ipairs(require("overseer").list_tasks({})) do
task:dispose(true)
end
end,
},
- post_restore_cmds = {
- function()
- overseer.load_task_bundle(get_cwd_as_name(), { ignore_missing = true })
- end,
- },
})
```
diff --git a/doc/tutorials.md b/doc/tutorials.md
index 29cda940..1b0165c1 100644
--- a/doc/tutorials.md
+++ b/doc/tutorials.md
@@ -15,15 +15,7 @@ If you're simply looking for the easiest way to define custom tasks, overseer su
In this tutorial, you will create a custom task that builds a C++ file.
-First, change your call to `setup()` to include the following option:
-
-```lua
-require("overseer").setup({
- templates = { "builtin", "user.cpp_build" },
-})
-```
-
-Next, create the file `lua/overseer/template/user/cpp_build.lua` inside your neovim config directory (`:echo stdpath('config')`). Add the following content:
+First, create the file `lua/overseer/template/user/cpp_build.lua` inside your neovim config directory (`:echo stdpath("config")`). Add the following content:
```lua
-- /home/stevearc/.config/nvim/lua/overseer/template/user/cpp_build.lua
@@ -33,11 +25,14 @@ return {
-- Full path to current file (see :help expand())
local file = vim.fn.expand("%:p")
return {
- cmd = { "g++" },
- args = { file },
+ cmd = { "g++", file },
+ -- attach a component to the task that will pipe the output to the quickfix.
+ -- components customize the behavior of a task.
+ -- see :help overseer-components for a list of all components.
components = { { "on_output_quickfix", open = true }, "default" },
}
end,
+ -- provide a condition so the task will only be available when you are in a c++ file
condition = {
filetype = { "cpp" },
},
@@ -52,15 +47,7 @@ Now when you are editing a cpp file, you can run `:OverseerRun` and select "g++
In this tutorial, you will create a task that re-runs a script every time it's saved, and view the output in a split. If there are errors, they will be displayed inline.
-First, change your call to `setup()` to include the following option:
-
-```lua
-require("overseer").setup({
- templates = { "builtin", "user.run_script" },
-})
-```
-
-Next, create the file `lua/overseer/template/user/run_script.lua` inside your neovim config directory (`:echo stdpath('config')`). Add the following content:
+First, create the file `lua/overseer/template/user/run_script.lua` inside your neovim config directory (`:echo stdpath('config')`). Add the following content:
```lua
-- /home/stevearc/.config/nvim/lua/overseer/template/user/run_script.lua
@@ -71,9 +58,13 @@ return {
local cmd = { file }
if vim.bo.filetype == "go" then
cmd = { "go", "run", file }
+ elseif vim.bo.filetype == "python" then
+ cmd = { "python", file }
end
return {
cmd = cmd,
+ -- add some components that will pipe the output to quickfix,
+ -- parse it using errorformat, and display any matching lines as diagnostics.
components = {
{ "on_output_quickfix", set_diagnostics = true },
"on_result_diagnostics",
@@ -87,7 +78,7 @@ return {
}
```
-Now open up a shell script or go file and run `:OverseerRun`. Select "run script".
+Now open up a shell script, go, or python file and run `:OverseerRun`. Select "run script".
If you want a test file to use, try the following go script:
@@ -111,27 +102,32 @@ set -e
echo "Hello world"
```
-The next step is to display the output in a vertical split. For that, we are going to use [actions](guides.md#actions). Run `:OverseerQuickAction` and select "open vsplit". This will open the output in a vertical split next to your file.
+The next step is to display the output in a vertical split. For that, we are going to use
+[actions](guides.md#actions). Run `:OverseerTaskAction`, select your task, and select "open vsplit". This will open
+the output in a vertical split next to your file.

-Try changing your script to have an error, then restart the task. The output should be updated, and you should see inline diagnostics for the error (see `:help vim.diagnostic`).
+Try changing your script to have an error, then restart the task (`:OverseerTaskAction` > task > restart).
+The output should be updated, and you should see inline diagnostics for the error (see `:help vim.diagnostic`).

-Lastly, we would like to restart this task every time we save the file. Once more use `:OverseerQuickAction` and this time select "watch". It will prompt you for a path to watch, you should enter the path to the file. Now every time you `:w` the file it should re-run and update the output!
+Lastly, we would like to restart this task every time we save the file. Once more use
+`:OverseerTaskAction` and this time select "watch". It will prompt you for a path to watch, you
+should enter the path to the file. Now every time you `:w` the file it should re-run and update the
+output!
Finally, you can create a custom command to do all of these steps at once:
```lua
vim.api.nvim_create_user_command("WatchRun", function()
local overseer = require("overseer")
- overseer.run_template({ name = "run script" }, function(task)
+ overseer.run_task({ name = "run script", autostart = false }, function(task)
if task then
- task:add_component({ "restart_on_save", paths = {vim.fn.expand("%:p")} })
- local main_win = vim.api.nvim_get_current_win()
- overseer.run_action(task, "open vsplit")
- vim.api.nvim_set_current_win(main_win)
+ task:add_component({ "restart_on_save", paths = { vim.fn.expand("%:p") } })
+ task:start()
+ task:open_output("vertical")
else
vim.notify("WatchRun not supported for filetype " .. vim.bo.filetype, vim.log.levels.ERROR)
end
@@ -141,4 +137,4 @@ end, {})
Now you can do `:WatchRun` on any supported file and it will run the file, open the output in a split, and re-run on save.
-Final note: to stop watching the file use the "dispose" action from `:OverseerQuickAction`.
+Final note: to stop watching the file use the "dispose" action from `:OverseerTaskAction`.
diff --git a/justfile b/justfile
index fb824084..9a4bae67 100644
--- a/justfile
+++ b/justfile
@@ -12,7 +12,7 @@ _private-task:
echo "Private task"
doc:
- python .github/main.py generate
+ venv/bin/python scripts/main.py generate
lint:
python .github/main.py lint
diff --git a/lua/lualine/components/overseer.lua b/lua/lualine/components/overseer.lua
index a1343e8c..369207c7 100644
--- a/lua/lualine/components/overseer.lua
+++ b/lua/lualine/components/overseer.lua
@@ -27,21 +27,14 @@
-- *unique* (default: false)
-- If true, ignore tasks with duplicate names.
--
--- *name* (default: nil)
--- String or list of strings. Only count tasks with this name or names.
---
--- *name_not* (default: false)
--- When true, count all tasks that do *not* match the 'name' param.
---
-- *status* (default: nil)
-- String or list of strings. Only count tasks with this status.
--
--- *status_not* (default: false)
--- When true, count all tasks that do *not* match the 'status' param.
+-- *filter* (default: nil)
+-- A filter function to apply to tasks. Only count tasks that pass the filter.
local M = require("lualine.component"):extend()
local constants = require("overseer.constants")
-local overseer = require("overseer")
local task_list = require("overseer.task_list")
local util = require("overseer.util")
local utils = require("lualine.utils.utils")
@@ -63,15 +56,11 @@ local default_no_icons = {
function M:init(options)
M.super.init(self, options)
+ self.options.include_ephemeral = self.options.include_ephemeral ~= false
self.options.label = self.options.label or ""
if self.options.colored == nil then
self.options.colored = true
end
- if self.options.colored then
- overseer.on_setup(function()
- self:update_colors()
- end)
- end
self.symbols = vim.tbl_extend(
"keep",
self.options.symbols or {},
diff --git a/lua/neotest/client/strategies/overseer.lua b/lua/neotest/client/strategies/overseer.lua
index 3c6a0596..425d1499 100644
--- a/lua/neotest/client/strategies/overseer.lua
+++ b/lua/neotest/client/strategies/overseer.lua
@@ -1,3 +1,4 @@
+local component = require("overseer.component")
local lib = require("neotest.lib")
local log = require("overseer.log")
local nio = require("nio")
@@ -20,7 +21,7 @@ M.recycle_group = function(group_id)
if not pool[group_id] then
pool[group_id] = {}
end
- log:debug("Recycling neotest task group %s", group_id)
+ log.debug("Recycling neotest task group %s", group_id)
vim.list_extend(pool[group_id], tasks_by_group[group_id])
tasks_by_group[group_id] = {}
end
@@ -41,7 +42,7 @@ local function get_or_create_task(spec, context, output_path)
end
if task then
-- Reset the task
- log:debug("Using pooled neotest task %s from group %s", task.id, current_group_id)
+ log.debug("Using pooled neotest task %s from group %s", task.id, current_group_id)
task:reset(false)
task:remove_components({ "on_output_write_file", "neotest.link_with_neotest" })
task:add_components({
@@ -75,12 +76,13 @@ local function get_or_create_task(spec, context, output_path)
opts.cmd = spec.command
opts.env = spec.env
opts.cwd = spec.cwd
+ opts.ephemeral = true
opts.metadata = {
neotest_group_id = current_group_id,
}
+ ---@cast opts overseer.TaskDefinition
task = overseer.new_task(opts)
- log:debug("Created new neotest task %s group %s", task.id, current_group_id)
- task:set_include_in_bundle(false)
+ log.debug("Created new neotest task %s group %s", task.id, current_group_id)
task:subscribe("on_dispose", function(disposed_task)
local tasks = tasks_by_group[disposed_task.metadata.group_id]
if tasks then
@@ -99,8 +101,8 @@ end
---@param context neotest.StrategyContext
---@return neotest.Process
local function get_strategy(spec, context)
- if not overseer.component.get_alias("default_neotest") then
- overseer.component.alias("default_neotest", { "default" })
+ if not component.get_alias("default_neotest") then
+ component.alias("default_neotest", { "default" })
end
local finish_future = nio.control.future()
diff --git a/lua/overseer/action_util.lua b/lua/overseer/action_util.lua
index 553af668..672d851a 100644
--- a/lua/overseer/action_util.lua
+++ b/lua/overseer/action_util.lua
@@ -1,50 +1,46 @@
+local actions = require("overseer.task_list.actions")
local config = require("overseer.config")
-local log = require("overseer.log")
local task_list = require("overseer.task_list")
local util = require("overseer.util")
local M = {}
+---@param opts {name?: string, pre_action: fun(task: overseer.Task), post_action: fun(task: overseer.Task)}
---@param task overseer.Task
----@param name? string
-M.run_task_action = function(task, name)
- M.run_action({
- actions = config.actions,
- name = name,
- prompt = string.format("Actions: %s", task.name),
- pre_action = function()
- task:inc_reference()
- end,
- post_action = function()
- task:dec_reference()
- task_list.update(task)
- end,
- }, task)
-end
+local function run_action(opts, task)
+ vim.validate("name", opts.name, "string", true)
+ vim.validate("pre_action", opts.post_action, "function", true)
+ vim.validate("post_action", opts.post_action, "function", true)
+
+ -- First merge the config actions with the builtins
+ local all_actions = {}
+ for k, v in pairs(actions) do
+ all_actions[k] = v
+ end
+ for k, v in pairs(config.actions) do
+ if v then
+ all_actions[k] = v
+ else
+ -- If the user set the action to false, remove it from the list
+ all_actions[k] = nil
+ end
+ end
-M.run_action = function(opts, ...)
- vim.validate({
- actions = { opts.actions, "t" },
- name = { opts.name, "s", true },
- prompt = { opts.prompt, "s" },
- pre_action = { opts.post_action, "f", true },
- post_action = { opts.post_action, "f", true },
- })
- local args = vim.F.pack_len(...)
local viable = {}
local longest_name = 1
- for k, action in pairs(opts.actions) do
- if action.condition == nil or action.condition(...) then
+ for k, action in pairs(all_actions) do
+ if action.condition == nil or action.condition(task) then
if k == opts.name then
if opts.pre_action then
- opts.pre_action(...)
+ opts.pre_action(task)
end
- action.run(...)
+ action.run(task)
if opts.post_action then
- opts.post_action(...)
+ opts.post_action(task)
end
return
end
+
action.name = k
local name_len = vim.api.nvim_strwidth(k)
if name_len > longest_name then
@@ -54,7 +50,7 @@ M.run_action = function(opts, ...)
end
end
if opts.name then
- log:warn("Cannot perform action '%s'", opts.name)
+ vim.notify(string.format("Cannot perform action '%s'", opts.name), vim.log.levels.ERROR)
return
end
table.sort(viable, function(a, b)
@@ -62,10 +58,10 @@ M.run_action = function(opts, ...)
end)
if opts.pre_action then
- opts.pre_action(...)
+ opts.pre_action(task)
end
vim.ui.select(viable, {
- prompt = opts.prompt,
+ prompt = string.format("Actions: %s", task.name),
kind = "overseer_task_options",
format_item = function(action)
if action.desc then
@@ -76,16 +72,34 @@ M.run_action = function(opts, ...)
end,
}, function(action)
if action then
- if action.condition == nil or action.condition(vim.F.unpack_len(args)) then
- action.run(vim.F.unpack_len(args))
+ if action.condition == nil or action.condition(task) then
+ action.run(task)
else
- log:warn("Can no longer perform action '%s' on task", action.name)
- end
- if opts.post_action then
- opts.post_action(vim.F.unpack_len(args))
+ vim.notify(
+ string.format("Can no longer perform action '%s' on task", action.name),
+ vim.log.levels.ERROR
+ )
end
end
+ if opts.post_action then
+ opts.post_action(task)
+ end
end)
end
+---@param task overseer.Task
+---@param name? string
+M.run_task_action = function(task, name)
+ run_action({
+ name = name,
+ pre_action = function()
+ task:inc_reference()
+ end,
+ post_action = function()
+ task:dec_reference()
+ task_list.touch(task)
+ end,
+ }, task)
+end
+
return M
diff --git a/lua/overseer/binding_util.lua b/lua/overseer/binding_util.lua
deleted file mode 100644
index 78c94e5c..00000000
--- a/lua/overseer/binding_util.lua
+++ /dev/null
@@ -1,133 +0,0 @@
-local config = require("overseer.config")
-local layout = require("overseer.layout")
-local util = require("overseer.util")
-local M = {}
-
----@diagnostic disable: undefined-field
-
-M.create_plug_bindings = function(bufnr, plug_bindings, ...)
- local args = vim.F.pack_len(...)
- for _, binding in ipairs(plug_bindings) do
- local rhs = binding.rhs
- if type(binding.rhs) == "function" then
- rhs = function()
- binding.rhs(vim.F.unpack_len(args))
- end
- end
- vim.keymap.set("", binding.plug, rhs, { buffer = bufnr, desc = binding.desc })
- end
-end
-
----@param bufnr number
----@param mode string
----@param bindings table
----@param prefix string
-M.create_bindings_to_plug = function(bufnr, mode, bindings, prefix)
- local maps
- if mode == "i" then
- maps = vim.api.nvim_buf_get_keymap(bufnr, "")
- end
- for lhs, rhs in pairs(bindings) do
- -- Prefix with unless this is a or :Cmd mapping
- if rhs then
- if type(rhs) == "string" and not rhs:match("[<:]") then
- rhs = "" .. prefix .. rhs
- end
- if mode == "i" then
- -- HACK for some reason I can't get plug mappings to work in insert mode
- for _, map in ipairs(maps) do
- if map.lhs == rhs then
- ---@diagnostic disable-next-line: cast-local-type
- rhs = map.callback or map.rhs
- break
- end
- end
- end
- vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, remap = true })
- end
- end
-end
-
----@param prefix string
-M.show_bindings = function(prefix)
- prefix = "" .. prefix
- local plug_to_bindings = {}
- local descriptions = {}
- local max_left = 1
- for _, keymap in ipairs(vim.api.nvim_buf_get_keymap(0, "n")) do
- if vim.startswith(keymap.lhs, prefix) then
- descriptions[keymap.lhs] = keymap.desc
- elseif keymap.rhs and vim.startswith(keymap.rhs, prefix) then
- max_left = math.max(max_left, vim.api.nvim_strwidth(keymap.lhs))
- local bindings = plug_to_bindings[keymap.rhs]
- if not bindings then
- bindings = {}
- plug_to_bindings[keymap.rhs] = bindings
- end
- table.insert(bindings, keymap.lhs)
- table.sort(bindings)
- end
- end
-
- local bindings_to_plug = {}
- local highlights = {}
- for plug, bindings in pairs(plug_to_bindings) do
- local binding_str = table.concat(bindings, "/")
- bindings_to_plug[binding_str] = plug
- local hl = {}
- highlights[binding_str] = hl
- local col_start = 0
- for _, binding in ipairs(bindings) do
- local col_end = col_start + binding:len() + 1
- table.insert(hl, { col_start, col_end })
- col_start = col_end + 1
- end
- end
-
- local lhs = vim.tbl_keys(bindings_to_plug)
- table.sort(lhs)
-
- local lines = {}
- local max_line = 1
- for _, left in ipairs(lhs) do
- local right = descriptions[bindings_to_plug[left]]
- local line = string.format(" %s %s", util.ljust(left, max_left), right)
- max_line = math.max(max_line, vim.api.nvim_strwidth(line))
- table.insert(lines, line)
- end
-
- local bufnr = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
- local ns = vim.api.nvim_create_namespace("overseer")
- for i, left in ipairs(lhs) do
- for _, hl in ipairs(highlights[left]) do
- local start_col, end_col = unpack(hl)
- vim.api.nvim_buf_set_extmark(bufnr, ns, i - 1, start_col, {
- end_col = end_col,
- hl_group = "Special",
- })
- end
- end
- vim.keymap.set("n", "q", "close", { buffer = bufnr })
- vim.keymap.set("n", "", "close", { buffer = bufnr })
- vim.bo[bufnr].modifiable = false
- vim.bo[bufnr].bufhidden = "wipe"
-
- local width = layout.calculate_width(max_line + 1, { min_width = 20 })
- local height = layout.calculate_height(#lines, { min_height = 10 })
- local win = vim.api.nvim_open_win(bufnr, true, {
- relative = "editor",
- row = math.floor((layout.get_editor_height() - height) / 2),
- col = math.floor((layout.get_editor_width() - width) / 2),
- width = width,
- height = height,
- style = "minimal",
- border = config.help_win.border,
- zindex = config.help_win.zindex,
- })
- for opt, value in pairs(config.help_win.win_opts or {}) do
- vim.wo[win][opt] = value
- end
-end
-
-return M
diff --git a/lua/overseer/commands.lua b/lua/overseer/commands.lua
index 74894d68..7249f3da 100644
--- a/lua/overseer/commands.lua
+++ b/lua/overseer/commands.lua
@@ -1,16 +1,8 @@
-local Task = require("overseer.task")
local action_util = require("overseer.action_util")
-local config = require("overseer.config")
local constants = require("overseer.constants")
-local files = require("overseer.files")
-local layout = require("overseer.layout")
local log = require("overseer.log")
-local sidebar = require("overseer.task_list.sidebar")
-local task_bundle = require("overseer.task_bundle")
-local task_editor = require("overseer.task_editor")
local task_list = require("overseer.task_list")
local template = require("overseer.template")
-local util = require("overseer.util")
local window = require("overseer.window")
local M = {}
@@ -31,121 +23,24 @@ M._toggle = function(params)
window.toggle({ enter = not params.bang, direction = args_or_nil(params.args) })
end
-M._save_bundle = function(params)
- task_bundle.save_task_bundle(args_or_nil(params.args))
-end
-
-M._load_bundle = function(params)
- task_bundle.load_task_bundle(args_or_nil(params.args), { autostart = not params.bang })
-end
-
-M._delete_bundle = function(params)
- task_bundle.delete_task_bundle(args_or_nil(params.args))
-end
-
-M._info = function(params)
- M.info(function(info)
- local lines = {}
- local highlights = {}
- if info.log.file then
- table.insert(lines, string.format("Log file: %s", info.log.file))
- end
- if info.log.level then
- table.insert(lines, string.format("Log level: %s", info.log.level))
- end
- if not vim.tbl_isempty(info.templates.templates) then
- table.insert(lines, "Individual templates")
- table.insert(highlights, { "Title", #lines, 0, -1 })
- end
- for name, tmpl_report in pairs(info.templates.templates) do
- if tmpl_report.is_present then
- table.insert(lines, string.format("%s: available", name))
- else
- table.insert(lines, string.format("%s: %s", name, tmpl_report.message))
- end
- table.insert(
- highlights,
- { tmpl_report.is_present and "OverseerSUCCESS" or "OverseerFAILURE", #lines, 0, name:len() }
- )
- end
- if not vim.tbl_isempty(info.templates.providers) then
- table.insert(lines, "Template providers")
- table.insert(highlights, { "Title", #lines, 0, -1 })
+M._run_shell = function(params)
+ if params.args and params.args ~= "" then
+ local task = require("overseer.task").new({
+ cmd = params.args,
+ })
+ if not params.bang then
+ task:start()
end
- for name, provider_report in pairs(info.templates.providers) do
- if provider_report.is_present then
- if provider_report.from_cache then
- name = name .. " (cached)"
+ else
+ vim.ui.input({ prompt = "command", completion = "shellcmdline" }, function(cmd)
+ if cmd then
+ local task = require("overseer.task").new({ cmd = cmd })
+ if not params.bang then
+ task:start()
end
- table.insert(
- lines,
- string.format(
- "%s: %d/%d tasks available",
- name,
- provider_report.available_tasks,
- provider_report.total_tasks
- )
- )
- else
- table.insert(lines, string.format("%s: %s", name, provider_report.message))
end
- table.insert(highlights, {
- provider_report.is_present and provider_report.available_tasks > 0 and "OverseerSUCCESS"
- or "OverseerFAILURE",
- #lines,
- 0,
- name:len(),
- })
- end
-
- local max_width = 0
- for _, line in ipairs(lines) do
- max_width = math.max(max_width, vim.api.nvim_strwidth(line))
- end
-
- local width = layout.calculate_width(max_width, { min_width = 80, max_width = 0.9 })
- local height = layout.calculate_height(#lines, { min_height = 10, max_height = 0.9 })
- local bufnr = vim.api.nvim_create_buf(false, true)
- local winid = vim.api.nvim_open_win(bufnr, true, {
- relative = "editor",
- border = config.form.border,
- zindex = config.form.zindex,
- width = width,
- height = height,
- col = math.floor((layout.get_editor_width() - width) / 2),
- row = math.floor((layout.get_editor_height() - height) / 2),
- style = "minimal",
- })
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
- vim.bo[bufnr].modifiable = false
- vim.bo[bufnr].modified = false
- vim.bo[bufnr].bufhidden = "wipe"
- vim.keymap.set("n", "q", "close", { buffer = bufnr })
- vim.keymap.set("n", "", "close", { buffer = bufnr })
- vim.api.nvim_create_autocmd("BufLeave", {
- desc = "Close info window when leaving buffer",
- buffer = bufnr,
- once = true,
- nested = true,
- callback = function()
- if vim.api.nvim_win_is_valid(winid) then
- vim.api.nvim_win_close(winid, true)
- end
- end,
- })
- local ns = vim.api.nvim_create_namespace("overseer")
- util.add_highlights(bufnr, ns, highlights)
- end)
-end
-
-M._run_command = function(params)
- local tmpl_params = {
- cmd = params.args ~= "" and params.args or nil,
- }
- M.run_template({
- name = "shell",
- params = tmpl_params,
- })
+ end)
+ end
end
M._run_template = function(params)
@@ -161,7 +56,7 @@ M._run_template = function(params)
end
end
if name and not vim.tbl_isempty(tags) then
- log:error("Cannot find template: %s is not a tag", name)
+ log.error("Cannot find template: %s is not a tag", name)
return
end
local opts = {
@@ -175,26 +70,10 @@ M._run_template = function(params)
end)
end
-M._build_task = function(_params)
- M.build_task()
-end
-
-M._quick_action = function(params)
- local action_name = params.fargs[1]
- if action_name == "" then
- action_name = nil
- end
- M.quick_action(action_name)
-end
-
M._task_action = function(params)
M.task_action()
end
-M._clear_cache = function(_params)
- M.clear_cache()
-end
-
---@return overseer.SearchParams
local function get_search_params()
-- If we have a file open, use its parent dir as the search dir.
@@ -226,38 +105,30 @@ end
-- TEMPLATE LOADING/RUNNING
---Options for running a template
----Values for prompt:
---- always Show when template has any params
---- missing Show when template has any params not explicitly passed in
---- allow Only show when a required param is missing
---- avoid Only show when a required param with no default value is missing
---- never Never show prompt (error if required param missing)
---@class overseer.TemplateRunOpts
---@field name? string The name of the template to run
---@field tags? string[] List of tags used to filter when searching for template
---@field autostart? boolean When true, start the task after creating it (default true)
---@field first? boolean When true, take first result and never show the task picker. Default behavior will auto-set this based on presence of name and tags
----@field prompt? "always"|"missing"|"allow"|"avoid"|"never" Controls when to prompt user for parameter input
---@field params? table Parameters to pass to template
---@field cwd? string Working directory for the task
---@field env? table Additional environment variables for the task
+---@field disallow_prompt? boolean When true, if any required parameters are missing return an error instead of prompting the user for them
+---@field on_build? fun(task_defn: overseer.TaskDefinition, util: overseer.TaskUtil) callback that is called after the task definition is built but before the task is created.
---@param opts overseer.TemplateRunOpts
---@param callback? fun(task: overseer.Task|nil, err: string|nil)
M.run_template = function(opts, callback)
opts = vim.tbl_deep_extend("keep", opts or {}, {
autostart = true,
- prompt = config.default_template_prompt,
- })
- vim.validate({
- name = { opts.name, "s", true },
- tags = { opts.tags, "t", true },
- autostart = { opts.autostart, "b", true },
- first = { opts.first, "b", true },
- prompt = { opts.prompt, "s", true },
- params = { opts.params, "t", true },
- callback = { callback, "f", true },
})
+ vim.validate("name", opts.name, "string", true)
+ vim.validate("tags", opts.tags, "table", true)
+ vim.validate("autostart", opts.autostart, "boolean", true)
+ vim.validate("first", opts.first, "boolean", true)
+ vim.validate("disallow_prompt", opts.disallow_prompt, "boolean", true)
+ vim.validate("params", opts.params, "table", true)
+ vim.validate("callback", callback, "function", true)
if opts.first == nil then
opts.first = opts.name ~= nil or not vim.tbl_isempty(opts.tags or {})
end
@@ -277,23 +148,16 @@ M.run_template = function(opts, callback)
return
end
local build_opts = {
- prompt = opts.prompt,
params = opts.params or {},
search = search_opts,
+ disallow_prompt = opts.disallow_prompt,
+ on_build = opts.on_build,
+ env = opts.env,
+ cwd = opts.cwd,
}
- template.build_task_args(tmpl, build_opts, function(task_defn)
- local task = nil
- if task_defn then
- if opts.cwd then
- task_defn.cwd = opts.cwd
- end
- if task_defn.env or opts.env then
- task_defn.env = vim.tbl_deep_extend("force", task_defn.env or {}, opts.env or {})
- end
- task = Task.new(task_defn)
- if opts.autostart then
- task:start()
- end
+ template.build_task(tmpl, build_opts, function(_, task)
+ if task and opts.autostart then
+ task:start()
end
if callback then
callback(task)
@@ -310,7 +174,7 @@ M.run_template = function(opts, callback)
end, templates)
if #templates == 0 then
- log:error("Could not find any matching task templates for opts %s", opts)
+ log.error("Could not find any matching task templates for opts %s", opts)
elseif #templates == 1 or opts.first then
handle_tmpl(templates[1])
else
@@ -334,46 +198,15 @@ M.run_template = function(opts, callback)
end
end
-M.build_task = function()
- local task = Task.new({
- name = "New task",
- cmd = { "ls" },
- })
- task_editor.open(task, function(result)
- if result then
- task:start()
- else
- task:dispose()
- end
- end)
-end
-
----@param name? string Name of action to run
-M.quick_action = function(name)
- if vim.bo.filetype == "OverseerList" then
- local sb = sidebar.get_or_create()
- sb:run_action(name)
- return
- end
- local tasks = task_list.list_tasks({ recent_first = true })
- local task
- if #tasks == 0 then
- vim.notify("No tasks available", vim.log.levels.WARN)
- return
- else
- task = tasks[1]
- end
- action_util.run_task_action(task, name)
-end
-
M.task_action = function()
- local tasks = task_list.list_tasks({ unique = true, recent_first = true })
+ local tasks = task_list.list_tasks({
+ unique = true,
+ sort = task_list.sort_finished_recently,
+ include_ephemeral = true,
+ })
if #tasks == 0 then
vim.notify("No tasks available", vim.log.levels.WARN)
return
- elseif #tasks == 1 then
- action_util.run_task_action(tasks[1])
- return
end
local task_summaries = vim.tbl_map(function(task)
@@ -393,38 +226,11 @@ M.task_action = function()
end)
end
+---@param callback fun(info: overseer.Report)
M.info = function(callback)
local search_opts = get_search_params()
- local info = {
- log = {
- file = nil,
- level = nil,
- },
- templates = {
- templates = {},
- providers = {},
- },
- }
- local levels_reverse = {}
- for k, v in pairs(vim.log.levels) do
- levels_reverse[v] = k
- end
- for _, log_conf in ipairs(config.log) do
- if log_conf.type == "file" then
- local ok, stdpath = pcall(vim.fn.stdpath, "log")
- if not ok then
- stdpath = vim.fn.stdpath("cache")
- end
- info.log = {
- file = files.join(stdpath, log_conf.filename),
- level = levels_reverse[log_conf.level],
- }
- break
- end
- end
template.list(search_opts, function(_, report)
- info.templates = report
- callback(info)
+ callback(report)
end)
end
diff --git a/lua/overseer/component/init.lua b/lua/overseer/component.lua
similarity index 68%
rename from lua/overseer/component/init.lua
rename to lua/overseer/component.lua
index 962c4fed..bae9ebc1 100644
--- a/lua/overseer/component/init.lua
+++ b/lua/overseer/component.lua
@@ -9,33 +9,32 @@ local M = {}
---`_*` means "triggers under some condition"
---`on__*` means "does something when is fired
---@class overseer.ComponentFileDefinition
----@field desc string
----@field long_desc? string
----@field params? overseer.Params
----@field editable? boolean
----@field serializable? boolean
----@field constructor fun(params: table): overseer.ComponentSkeleton
----@field deprecated_message? string
+---@field desc string description of component
+---@field long_desc? string extended description for documentation generation
+---@field params? overseer.Params parameters that can customize the component
+---@field constructor fun(params: table): overseer.ComponentSkeleton creates the component from the params
+---@field editable? boolean when true, component can be live-edited in the task editor
+---@field serializable? boolean when true, will be serialized when serializing a task
+---@field deprecated_message? string when present, overseer will warn the user when this component is used
---@class overseer.ComponentDefinition : overseer.ComponentFileDefinition
---@field name string
---The intermediate form of a component returned by the constructor
---@class overseer.ComponentSkeleton
----@field on_init? fun(self: overseer.Component, task: overseer.Task)
----@field on_pre_start? fun(self: overseer.Component, task: overseer.Task): nil|boolean
----@field on_start? fun(self: overseer.Component, task: overseer.Task)
----@field on_reset? fun(self: overseer.Component, task: overseer.Task)
----@field on_pre_result? fun(self: overseer.Component, task: overseer.Task): nil|table
----@field on_preprocess_result? fun(self: overseer.Component, task: overseer.Task, result: table)
----@field on_result? fun(self: overseer.Component, task: overseer.Task, result: table)
----@field on_complete? fun(self: overseer.Component, task: overseer.Task, status: overseer.Status, result: table)
----@field on_output? fun(self: overseer.Component, task: overseer.Task, data: string[])
----@field on_output_lines? fun(self: overseer.Component, task: overseer.Task, lines: string[])
----@field on_exit? fun(self: overseer.Component, task: overseer.Task, code: number)
----@field on_dispose? fun(self: overseer.Component, task: overseer.Task)
+---@field on_init? fun(self: overseer.Component, task: overseer.Task) called when the component is first created.
+---@field on_pre_start? fun(self: overseer.Component, task: overseer.Task): nil|boolean called when a task is attempting to start. Can return false to prevent the task from starting.
+---@field on_start? fun(self: overseer.Component, task: overseer.Task) called when the task has started
+---@field on_reset? fun(self: overseer.Component, task: overseer.Task) called when the task is reset
+---@field on_pre_result? fun(self: overseer.Component, task: overseer.Task): nil|table called when the task is generating results. Can return a table that will be merged into the task's results.
+---@field on_preprocess_result? fun(self: overseer.Component, task: overseer.Task, result: table) called after on_pre_result and before on_result. Can modify the result table.
+---@field on_result? fun(self: overseer.Component, task: overseer.Task, result: table) called after the task result has been created
+---@field on_complete? fun(self: overseer.Component, task: overseer.Task, status: overseer.Status, result: table) called when the task completes (successful or not)
+---@field on_output? fun(self: overseer.Component, task: overseer.Task, data: string[]) called with the raw output from jobstart on_stdout callback
+---@field on_output_lines? fun(self: overseer.Component, task: overseer.Task, lines: string[]) called with lines of text from the output. This is easier to work with than the raw data from on_output.
+---@field on_exit? fun(self: overseer.Component, task: overseer.Task, code: number) called when the process exits
+---@field on_dispose? fun(self: overseer.Component, task: overseer.Task) called when the task is disposed or the component is removed. Guaranteed to be called if on_init was called.
---@field on_status? fun(self: overseer.Component, task: overseer.Task, status: overseer.Status) Called when the task status changes
----@field render? fun(self: overseer.Component, task: overseer.Task, lines: string[], highlights: table[], detail: number)
---An instantiated component that is attached to a Task
---@class overseer.Component : overseer.ComponentSkeleton
@@ -46,41 +45,16 @@ local M = {}
---@field editable boolean
local registry = {}
-local aliases = {}
-
-local builtin_components = {
- "dependencies",
- "display_duration",
- "on_complete_dispose",
- "on_complete_notify",
- "on_complete_restart",
- "on_exit_set_status",
- "on_output_parse",
- "on_output_quickfix",
- "on_output_summarize",
- "on_output_write_file",
- "on_result_diagnostics",
- "on_result_diagnostics_quickfix",
- "on_result_diagnostics_trouble",
- "on_result_notify",
- "open_output",
- "restart_on_save",
- "run_after",
- "timeout",
- "unique",
-}
---@param name string
---@param opts overseer.ComponentDefinition
---@return overseer.Component
local function validate_component(name, opts)
- vim.validate({
- desc = { opts.desc, "s", true },
- params = { opts.params, "t", true },
- constructor = { opts.constructor, "f" },
- editable = { opts.editable, "b", true },
- serializable = { opts.serializable, "b", true },
- })
+ vim.validate("opts.desc", opts.desc, "string", true)
+ vim.validate("opts.params", opts.params, "table", true)
+ vim.validate("opts.constructor", opts.constructor, "function")
+ vim.validate("opts.editable", opts.editable, "boolean", true)
+ vim.validate("opts.serializable", opts.serializable, "boolean", true)
---@type overseer.Component
local comp = vim.deepcopy(opts) ---@diagnostic disable-line: assign-type-mismatch
if comp.serializable == nil then
@@ -104,39 +78,52 @@ local function validate_component(name, opts)
comp.editable = true
end
comp.name = name
- if opts.deprecated_message then
- log:warn("Overseer component %s is deprecated: %s", name, opts.deprecated_message)
- end
return comp
end
---@param name string
---@param components string[]
-M.alias = function(name, components)
- vim.validate({
- name = { name, "s" },
- components = { components, "t" },
- })
+---@param override? boolean
+M.alias = function(name, components, override)
+ if override or not config.component_aliases[name] then
+ config.component_aliases[name] = components
+ end
+end
- aliases[name] = components
+---@param name string
+local function load(name)
+ if not registry[name] then
+ local ok, mod = pcall(require, string.format("overseer.component.%s", name))
+ if ok then
+ registry[name] = validate_component(name, mod)
+ end
+ end
end
---@param name string
---@return overseer.ComponentDefinition?
M.get = function(name)
if not registry[name] then
+ load(name)
local ok, mod = pcall(require, string.format("overseer.component.%s", name))
if ok then
registry[name] = validate_component(name, mod)
end
end
- return registry[name]
+ local comp = registry[name]
+ if comp and comp.deprecated_message then
+ vim.notify_once(
+ string.format("Overseer component %s is deprecated: %s", name, comp.deprecated_message),
+ vim.log.levels.WARN
+ )
+ end
+ return comp
end
---@param name string
---@return string[]?
M.get_alias = function(name)
- return aliases[name]
+ return config.component_aliases[name]
end
---@param comp_params overseer.Serialized
@@ -150,23 +137,31 @@ end
---@return string
M.stringify_alias = function(name)
local strings = {}
- for _, comp in ipairs(aliases[name]) do
+ for _, comp in ipairs(M.get_alias(name) or {}) do
table.insert(strings, getname(comp))
end
return table.concat(strings, ", ")
end
+local preloaded = false
+local function preload_components()
+ if preloaded then
+ return
+ end
+ preloaded = true
+ local comp_files = vim.api.nvim_get_runtime_file("lua/overseer/component/*.lua", true)
+ for _, abspath in ipairs(comp_files) do
+ local module_name = abspath:match("^.*overseer/component/(.*)%.lua$")
+ load(module_name)
+ end
+end
+
---@return string[]
M.list_editable = function()
local ret = {}
- for _, v in ipairs(config.preload_components) do
- M.get(v)
- end
- for _, v in ipairs(builtin_components) do
- M.get(v)
- end
+ preload_components()
for k, v in pairs(registry) do
- if v.editable then
+ if v.editable and not v.deprecated_message then
table.insert(ret, k)
end
end
@@ -175,7 +170,7 @@ end
---@return string[]
M.list_aliases = function()
- return vim.tbl_keys(aliases)
+ return vim.tbl_keys(config.component_aliases)
end
M.params_should_replace = function(new_params, existing)
@@ -197,8 +192,9 @@ local function resolve(seen, resolved, names)
-- Let's not get stuck if there are cycles
if not seen[name] then
seen[name] = true
- if aliases[name] then
- resolve(seen, resolved, aliases[name])
+ local alias_components = M.get_alias(name)
+ if alias_components then
+ resolve(seen, resolved, alias_components)
else
table.insert(resolved, comp_params)
end
@@ -231,7 +227,7 @@ local function validate_params(params, schema, ignore_errors)
end
for name in pairs(params) do
if type(name) == "string" and (not schema or schema[name] == nil) then
- log:warn("Component '%s' passed unknown param '%s'", getname(params), name)
+ log.warn("Component '%s' passed unknown param '%s'", getname(params), name)
params[name] = nil
end
end
@@ -275,10 +271,8 @@ end
---@param existing nil|overseer.Serialized[] A list of instantiated components or component params
---@return overseer.Serialized[]
M.resolve = function(components, existing)
- vim.validate({
- components = { components, "t" },
- existing = { existing, "t", true },
- })
+ vim.validate("components", components, "table")
+ vim.validate("existing", existing, "table", true)
local seen = {}
if existing then
for _, comp in ipairs(existing) do
@@ -338,14 +332,19 @@ end
---@private
M.get_all_descriptions = function()
local ret = {}
- for _, name in ipairs(builtin_components) do
- local defn = assert(M.get(name))
- table.insert(ret, {
- name = name,
- desc = defn.desc,
- long_desc = defn.long_desc,
- params = simplify_params(defn.params),
- })
+ preload_components()
+ local names = vim.tbl_keys(registry)
+ table.sort(names)
+ for _, name in ipairs(names) do
+ local defn = registry[name]
+ if not defn.deprecated_message then
+ table.insert(ret, {
+ name = name,
+ desc = defn.desc,
+ long_desc = defn.long_desc,
+ params = simplify_params(defn.params),
+ })
+ end
end
return ret
end
diff --git a/lua/overseer/component/dependencies.lua b/lua/overseer/component/dependencies.lua
index 1a62d567..abad8f18 100644
--- a/lua/overseer/component/dependencies.lua
+++ b/lua/overseer/component/dependencies.lua
@@ -4,14 +4,24 @@ local task_list = require("overseer.task_list")
local util = require("overseer.util")
local STATUS = constants.STATUS
+---@type overseer.ComponentFileDefinition
return {
desc = "Set dependencies for task",
params = {
+ tasks = {
+ desc = "Names of dependency task templates",
+ long_desc = 'This can be a list of strings (template names, e.g. "cargo build"), tables (template name with params, e.g. {"mytask", foo = "bar"}), or tables (raw task params, e.g. {cmd = "sleep 10"})',
+ -- TODO Can't input dependencies WITH params in the task launcher b/c the type is too complex
+ type = "list",
+ optional = true,
+ },
task_names = {
+ deprecated = true,
desc = "Names of dependency task templates",
- long_desc = 'This can be a list of strings (template names, e.g. {"cargo build"}), tables (name with params, e.g. {"shell", cmd = "sleep 10"}), or tables (raw task params, e.g. {cmd = "sleep 10"})',
+ long_desc = 'This can be a list of strings (template names, e.g. "cargo build"), tables (template name with params, e.g. {"mytask", foo = "bar"}), or tables (raw task params, e.g. {cmd = "sleep 10"})',
-- TODO Can't input dependencies WITH params in the task launcher b/c the type is too complex
type = "list",
+ optional = true,
},
sequential = {
type = "boolean",
@@ -23,14 +33,14 @@ return {
task_lookup = {},
on_pre_start = function(self, task)
local started_any = false
- for i, name_or_config in ipairs(params.task_names) do
+ for i, name_or_config in ipairs(params.tasks or params.task_names or {}) do
local task_id = self.task_lookup[i]
local dep_task = task_id and task_list.get(task_id)
if not dep_task then
-- If no task ID found, start the dependency
util.run_template_or_task(name_or_config, function(new_task)
if not new_task then
- log:error(
+ log.error(
"Task(%s)[dependencies] could not find template %s",
task.name,
name_or_config
@@ -41,13 +51,12 @@ return {
new_task.env = new_task.env or task.env
new_task.parent_id = task.parent_id or task.id
self.task_lookup[i] = new_task.id
- new_task:add_component({ "on_success_complete_dependency", task_id = task.id })
- -- Don't include child tasks when saving to bundle.
- -- We will re-create them when this task runs again
- new_task:set_include_in_bundle(false)
+ new_task:add_component({
+ "dependencies.on_success_complete_dependency",
+ task_id = task.id,
+ })
+ new_task.ephemeral = true
new_task:start()
- -- Ensure this task is marked as more recent than its dependencies
- task_list.touch_task(task)
end)
started_any = true
if params.sequential then
diff --git a/lua/overseer/component/on_success_complete_dependency.lua b/lua/overseer/component/dependencies/on_success_complete_dependency.lua
similarity index 92%
rename from lua/overseer/component/on_success_complete_dependency.lua
rename to lua/overseer/component/dependencies/on_success_complete_dependency.lua
index 33009046..1c228d3c 100644
--- a/lua/overseer/component/on_success_complete_dependency.lua
+++ b/lua/overseer/component/dependencies/on_success_complete_dependency.lua
@@ -4,7 +4,7 @@ local task_list = require("overseer.task_list")
local STATUS = constants.STATUS
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "Run another task on status change",
params = {
task_id = {
@@ -28,7 +28,7 @@ local comp = {
if next then
next:dispatch("on_dependency_complete", task.id)
else
- log:warn("Could not find task %s", params.task_id)
+ log.warn("Could not find task %s", params.task_id)
end
if params.once then
vim.defer_fn(function()
@@ -39,5 +39,3 @@ local comp = {
}
end,
}
-
-return comp
diff --git a/lua/overseer/component/display_duration.lua b/lua/overseer/component/display_duration.lua
index 757c196d..b6682d42 100644
--- a/lua/overseer/component/display_duration.lua
+++ b/lua/overseer/component/display_duration.lua
@@ -1,54 +1,8 @@
-local task_list = require("overseer.task_list")
-local util = require("overseer.util")
-
-local timer
-
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "Display the run duration",
- params = {
- detail_level = {
- desc = "Show the duration at this detail level",
- type = "integer",
- default = 1,
- validate = function(v)
- return v >= 1 and v <= 3
- end,
- },
- },
- constructor = function(params)
- return {
- duration = nil,
- start_time = nil,
- on_reset = function(self, task)
- self.duration = nil
- self.start_time = nil
- end,
- on_start = function(self)
- if not timer then
- timer = assert(vim.loop.new_timer())
- timer:start(
- 1000,
- 1000,
- vim.schedule_wrap(function()
- task_list.rerender()
- end)
- )
- end
- self.start_time = os.time()
- end,
- on_complete = function(self)
- self.duration = os.time() - self.start_time
- end,
- render = function(self, task, lines, highlights, detail)
- if detail < params.detail_level or (not self.duration and not self.start_time) then
- return
- end
- local duration = self.duration or os.time() - self.start_time
- table.insert(lines, util.format_duration(duration))
- end,
- }
+ deprecated_message = "Components are no longer used to customize task rendering",
+ constructor = function()
+ return {}
end,
}
-
-return comp
diff --git a/lua/overseer/component/on_complete_dispose.lua b/lua/overseer/component/on_complete_dispose.lua
index 4ba7ff7d..b234ed7c 100644
--- a/lua/overseer/component/on_complete_dispose.lua
+++ b/lua/overseer/component/on_complete_dispose.lua
@@ -1,4 +1,3 @@
-local uv = vim.uv or vim.loop
local constants = require("overseer.constants")
local log = require("overseer.log")
local STATUS = constants.STATUS
@@ -15,7 +14,7 @@ local function is_buffer_visible(bufnr)
end
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "After task is completed, dispose it after a timeout",
params = {
timeout = {
@@ -47,9 +46,7 @@ local comp = {
},
constructor = function(opts)
opts = opts or {}
- vim.validate({
- timeout = { opts.timeout, "n" },
- })
+ vim.validate("timeout", opts.timeout, "number")
return {
timer = nil,
@@ -67,12 +64,12 @@ local comp = {
end,
_start_timer = function(self, task)
self:_stop_timer()
- log:debug(
+ log.debug(
"task(%s)[on_complete_dispose] starting dispose timer for %ds",
task.id,
opts.timeout
)
- self.timer = uv.new_timer()
+ self.timer = vim.uv.new_timer()
-- Start a repeating timer because the dispose could fail with a
-- temporary reason (e.g. the task buffer is open, or the action menu is
-- displayed for the task)
@@ -80,7 +77,7 @@ local comp = {
1000 * opts.timeout,
1000 * opts.timeout,
vim.schedule_wrap(function()
- log:debug("task(%s)[on_complete_dispose] attempt dispose", task.id)
+ log.debug("task(%s)[on_complete_dispose] attempt dispose", task.id)
task:dispose()
end)
)
@@ -88,7 +85,7 @@ local comp = {
on_complete = function(self, task, status)
if not vim.tbl_contains(opts.statuses, task.status) then
- log:debug(
+ log.debug(
"task(%s)[on_complete_dispose] complete, not auto-disposing task of status %s",
task.id,
status
@@ -103,7 +100,7 @@ local comp = {
then
self:_start_timer(task)
else
- log:debug(
+ log.debug(
"task(%s)[on_complete_dispose] complete, waiting for output view",
task.id,
status
@@ -132,5 +129,3 @@ local comp = {
}
end,
}
-
-return comp
diff --git a/lua/overseer/component/on_complete_notify.lua b/lua/overseer/component/on_complete_notify.lua
index cd307e2c..1cb78334 100644
--- a/lua/overseer/component/on_complete_notify.lua
+++ b/lua/overseer/component/on_complete_notify.lua
@@ -4,7 +4,7 @@ local util = require("overseer.util")
local STATUS = constants.STATUS
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "vim.notify when task is completed",
params = {
statuses = {
@@ -57,4 +57,3 @@ local comp = {
}
end,
}
-return comp
diff --git a/lua/overseer/component/on_complete_restart.lua b/lua/overseer/component/on_complete_restart.lua
index 32ffa065..97a10d77 100644
--- a/lua/overseer/component/on_complete_restart.lua
+++ b/lua/overseer/component/on_complete_restart.lua
@@ -4,7 +4,7 @@ local util = require("overseer.util")
local STATUS = constants.STATUS
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "Restart task when it completes",
params = {
statuses = {
@@ -44,4 +44,3 @@ local comp = {
}
end,
}
-return comp
diff --git a/lua/overseer/component/on_exit_set_status.lua b/lua/overseer/component/on_exit_set_status.lua
index 360bb9f2..646d2539 100644
--- a/lua/overseer/component/on_exit_set_status.lua
+++ b/lua/overseer/component/on_exit_set_status.lua
@@ -2,7 +2,7 @@ local constants = require("overseer.constants")
local STATUS = constants.STATUS
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "Sets final task status based on exit code",
params = {
success_codes = {
@@ -25,4 +25,3 @@ local comp = {
}
end,
}
-return comp
diff --git a/lua/overseer/component/on_output_notify.lua b/lua/overseer/component/on_output_notify.lua
index a3561d86..2fd89a96 100644
--- a/lua/overseer/component/on_output_notify.lua
+++ b/lua/overseer/component/on_output_notify.lua
@@ -1,5 +1,4 @@
local util = require("overseer.util")
-local uv = vim.uv or vim.loop
local function has_nvim_notify()
return not not pcall(require, "notify")
@@ -13,11 +12,11 @@ local function get_notify_config(setting, default)
end
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "Use nvim-notify to show notification with task output summary for long-running tasks",
long_desc = vim.trim([[
-Works like on_complete_notify but, for long-running commands, also shows real-time output summary (like on_output_summarize).
+Works like on_complete_notify but, for long-running commands, also shows real-time output summary.
Requires nvim-notify to modify the last notification window when new output arrives instead of creating new notification.
]]),
@@ -122,11 +121,11 @@ When output_on_complete==false: shows status + last output lines during task run
end,
on_start = function(self)
- self.start_time = uv.now()
+ self.start_time = vim.uv.now()
end,
on_output = function(self, task, _data)
- local elapsed = uv.now() - self.start_time
+ local elapsed = vim.uv.now() - self.start_time
if elapsed < params.delay_ms then
return
end
@@ -146,5 +145,3 @@ When output_on_complete==false: shows status + last output lines during task run
}
end,
}
-
-return comp
diff --git a/lua/overseer/component/on_output_parse.lua b/lua/overseer/component/on_output_parse.lua
index 634b2b27..350dbcc8 100644
--- a/lua/overseer/component/on_output_parse.lua
+++ b/lua/overseer/component/on_output_parse.lua
@@ -1,28 +1,46 @@
local files = require("overseer.files")
local log = require("overseer.log")
-local parser = require("overseer.parser")
-local problem_matcher = require("overseer.template.vscode.problem_matcher")
+local parselib = require("overseer.parselib")
+local problem_matcher = require("overseer.vscode.problem_matcher")
+
+---@param cwd string
+---@param result table
+---@return table
+local function fix_relative_filenames(cwd, result)
+ if result.diagnostics then
+ -- Ensure that all relative filenames are rooted at the task cwd, not vim's current cwd
+ for _, diag in ipairs(result.diagnostics) do
+ if diag.filename and not files.is_absolute(diag.filename) then
+ diag.filename = vim.fs.joinpath(cwd, diag.filename)
+ end
+ end
+ end
+ return result
+end
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "Parses task output and sets task result",
params = {
parser = {
- desc = "Parser definition to extract values from output",
+ desc = "Parse function or overseer.OutputParser",
+ long_desc = "This can be a function that takes a line of output and (optionally) returns a quickfix-list item (see :help |setqflist-what|). For more complex parsing, this should be a class of type overseer.OutputParser.",
type = "opaque",
optional = true,
order = 1,
},
problem_matcher = {
desc = "VS Code-style problem matcher",
+ long_desc = "Only one of 'parser', 'problem_matcher', or 'errorformat' is allowed.",
type = "opaque",
optional = true,
order = 2,
},
- relative_file_root = {
- desc = "Relative filepaths will be joined to this root (instead of task cwd)",
+ errorformat = {
+ desc = "Errorformat string",
+ long_desc = "Only one of 'parser', 'problem_matcher', or 'errorformat' is allowed.",
+ type = "opaque",
optional = true,
- default_from_task = true,
order = 3,
},
precalculated_vars = {
@@ -32,70 +50,68 @@ local comp = {
optional = true,
order = 4,
},
+ relative_file_root = {
+ desc = "Relative filepaths will be joined to this root (instead of task cwd)",
+ optional = true,
+ default_from_task = true,
+ order = 5,
+ },
},
constructor = function(params)
- if params.parser and params.problem_matcher then
- log:warn("on_output_parse: cannot specify both 'parser' and 'problem_matcher'")
- elseif not params.parser and not params.problem_matcher then
- log:error("on_output_parse: one of 'parser', 'problem_matcher' is required")
+ local p = { params.parser, params.problem_matcher, params.errorformat }
+ local num_parse_opts = #vim.tbl_keys(p)
+ if num_parse_opts == 0 then
+ log.error("on_output_parse: one of 'parser', 'problem_matcher', 'errorformat' is required")
+ return {}
+ elseif num_parse_opts > 1 then
+ log.error(
+ "on_output_parse: only one of 'parser', 'problem_matcher', 'errorformat' is allowed"
+ )
return {}
end
- local parser_defn = params.parser
+
+ local parser
if params.problem_matcher then
- local pm = problem_matcher.resolve_problem_matcher(params.problem_matcher)
- if pm then
- parser_defn = problem_matcher.get_parser_from_problem_matcher(pm, params.precalculated_vars)
- if parser_defn then
- parser_defn = { diagnostics = parser_defn }
- end
- end
+ parser = problem_matcher.get_parser_from_problem_matcher(
+ params.problem_matcher,
+ params.precalculated_vars
+ )
+ elseif type(params.parser) == "function" then
+ parser = parselib.make_parser(params.parser)
+ elseif params.parser then
+ parser = params.parser
+ else
+ parser = parselib.parser_from_errorformat(params.errorformat)
end
- if not parser_defn then
+ if not parser then
+ log.error(
+ "Could not create output parser from %s",
+ params.problem_matcher or params.parser or params.errorformat
+ )
return {}
end
+ ---@cast parser overseer.OutputParser
+
+ local version = parser.result_version
return {
- on_init = function(self, task)
- self.parser = parser.new(parser_defn)
- self.parser_sub = function(key, result)
- -- TODO reconsider this API for dispatching partial results
- -- task:dispatch("on_stream_result", key, result)
- end
- self.parser:subscribe("new_item", self.parser_sub)
- self.set_results_sub = function()
- local result = self.parser:get_result()
- if result.diagnostics then
- -- Ensure that all relative filenames are rooted at the task cwd, not vim's current cwd
- for _, diag in ipairs(result.diagnostics) do
- if diag.filename and not files.is_absolute(diag.filename) then
- diag.filename = files.join(params.relative_file_root or task.cwd, diag.filename)
- end
- end
- end
- task:set_result(result)
- end
- self.parser:subscribe("set_results", self.set_results_sub)
- end,
- on_dispose = function(self)
- if self.parser_sub then
- self.parser:unsubscribe("new_item", self.parser_sub)
- self.parser_sub = nil
- end
- if self.set_results_sub then
- self.parser:unsubscribe("set_results", self.set_results_sub)
- self.set_results_sub = nil
- end
- end,
on_reset = function(self)
- self.parser:reset()
+ parser:reset()
+ version = parser.result_version
end,
on_output_lines = function(self, task, lines)
- self.parser:ingest(lines)
+ for _, line in ipairs(lines) do
+ parser:parse(line)
+ end
+ if version ~= parser.result_version then
+ task:set_result(
+ fix_relative_filenames(params.relative_file_root or task.cwd, parser:get_result())
+ )
+ version = parser.result_version
+ end
end,
on_pre_result = function(self, task)
- return self.parser:get_result()
+ return fix_relative_filenames(params.relative_file_root or task.cwd, parser:get_result())
end,
}
end,
}
-
-return comp
diff --git a/lua/overseer/component/on_output_quickfix.lua b/lua/overseer/component/on_output_quickfix.lua
index 16671f86..51713283 100644
--- a/lua/overseer/component/on_output_quickfix.lua
+++ b/lua/overseer/component/on_output_quickfix.lua
@@ -34,7 +34,7 @@ local function copen(self, height)
end
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "Set all task output into the quickfix (on complete)",
params = {
errorformat = {
@@ -119,7 +119,7 @@ local comp = {
if params.tail then
-- If we have been tailing the output, we should just keep the quickfix as it is
-- because we've exceeded the scrollback limit and will lose the earlier data.
- log:warn(
+ log.warn(
"Task(%d) '%s' exceeded the output scrollback limit (%d lines). Keeping tail output instead of doing a large replace operation upon completion.",
task.id,
task.name,
@@ -127,7 +127,7 @@ local comp = {
)
return
else
- log:warn(
+ log.warn(
"Task(%d) '%s' exceeded the output scrollback limit (%d lines). Only the last lines will be processed for the quickfix.",
task.id,
task.name,
@@ -238,5 +238,3 @@ local comp = {
return comp
end,
}
-
-return comp
diff --git a/lua/overseer/component/on_output_summarize.lua b/lua/overseer/component/on_output_summarize.lua
index fab8aa34..a042f64e 100644
--- a/lua/overseer/component/on_output_summarize.lua
+++ b/lua/overseer/component/on_output_summarize.lua
@@ -1,60 +1,8 @@
-local task_list = require("overseer.task_list")
-local util = require("overseer.util")
-
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "Summarize task output in the task list",
- params = {
- max_lines = {
- desc = "Number of lines of output to show when detail > 1",
- type = "integer",
- default = 4,
- validate = function(v)
- return v > 0
- end,
- },
- },
- constructor = function(params)
- return {
- lines = {},
- defer_update_lines = util.debounce(function(self, task, bufnr, num_lines)
- if vim.api.nvim_buf_is_valid(bufnr) then
- self.lines = util.get_last_output_lines(bufnr, num_lines)
- task_list.update(task)
- end
- end, {
- delay = 10,
- reset_timer_on_call = true,
- }),
- on_reset = function(self)
- self.lines = {}
- end,
- on_output = function(self, task, data)
- local bufnr = task:get_bufnr()
- self.lines = util.get_last_output_lines(bufnr, params.max_lines)
- -- Update again after delay because the terminal buffer takes a few millis to be updated
- -- after output is received
- self.defer_update_lines(self, task, bufnr, params.max_lines)
- end,
- render = function(self, task, lines, highlights, detail)
- local prefix = "out: "
- if detail == 1 then
- local last_line = self.lines[#self.lines]
- if last_line and last_line ~= "" then
- table.insert(lines, prefix .. last_line)
- table.insert(highlights, { "Comment", #lines, 0, 4 })
- table.insert(highlights, { "OverseerOutput", #lines, 4, -1 })
- end
- else
- for _, line in ipairs(self.lines) do
- table.insert(lines, prefix .. line)
- table.insert(highlights, { "Comment", #lines, 0, 4 })
- table.insert(highlights, { "OverseerOutput", #lines, 4, -1 })
- end
- end
- end,
- }
+ deprecated_message = "Components are no longer used to customize task rendering",
+ constructor = function()
+ return {}
end,
}
-
-return comp
diff --git a/lua/overseer/component/on_output_write_file.lua b/lua/overseer/component/on_output_write_file.lua
index 90ec2c52..7c66b3fd 100644
--- a/lua/overseer/component/on_output_write_file.lua
+++ b/lua/overseer/component/on_output_write_file.lua
@@ -1,5 +1,5 @@
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "Write task output to a file",
params = {
filename = {
@@ -36,5 +36,3 @@ local comp = {
}
end,
}
-
-return comp
diff --git a/lua/overseer/component/on_result_diagnostics.lua b/lua/overseer/component/on_result_diagnostics.lua
index f6aa6993..d9660750 100644
--- a/lua/overseer/component/on_result_diagnostics.lua
+++ b/lua/overseer/component/on_result_diagnostics.lua
@@ -13,7 +13,7 @@ local type_to_severity = {
-- Looks for a result value of 'diagnostics' that is a list of quickfix items
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "If task result contains diagnostics, display them",
params = {
virtual_text = {
@@ -109,5 +109,3 @@ local comp = {
}
end,
}
-
-return comp
diff --git a/lua/overseer/component/on_result_diagnostics_quickfix.lua b/lua/overseer/component/on_result_diagnostics_quickfix.lua
index df70782e..036b0aba 100644
--- a/lua/overseer/component/on_result_diagnostics_quickfix.lua
+++ b/lua/overseer/component/on_result_diagnostics_quickfix.lua
@@ -1,6 +1,8 @@
+local util = require("overseer.util")
+
-- Looks for a result value of 'diagnostics' that is a list of quickfix items
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "If task result contains diagnostics, add them to the quickfix",
params = {
use_loclist = {
@@ -67,12 +69,12 @@ local comp = {
end
elseif params.open then
local winid = vim.api.nvim_get_current_win()
- vim.cmd(conf.open_cmd)
- vim.api.nvim_set_current_win(winid)
+ util.eventignore_call(function()
+ vim.cmd(conf.open_cmd)
+ vim.api.nvim_set_current_win(winid)
+ end)
end
end,
}
end,
}
-
-return comp
diff --git a/lua/overseer/component/on_result_diagnostics_trouble.lua b/lua/overseer/component/on_result_diagnostics_trouble.lua
index 6029ae4b..baa42226 100644
--- a/lua/overseer/component/on_result_diagnostics_trouble.lua
+++ b/lua/overseer/component/on_result_diagnostics_trouble.lua
@@ -1,5 +1,5 @@
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "If task result contains diagnostics, open trouble.nvim",
params = {
args = {
@@ -37,5 +37,3 @@ local comp = {
}
end,
}
-
-return comp
diff --git a/lua/overseer/component/on_result_notify.lua b/lua/overseer/component/on_result_notify.lua
index 1c74ffe9..c2a051c2 100644
--- a/lua/overseer/component/on_result_notify.lua
+++ b/lua/overseer/component/on_result_notify.lua
@@ -4,7 +4,7 @@ local util = require("overseer.util")
local STATUS = constants.STATUS
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "vim.notify when task receives results",
long_desc = "Normally you will want to use on_complete_notify. If you have a long-running watch task (e.g. `tsc --watch`) that produces new results periodically, then this is the component you want.",
params = {
@@ -50,5 +50,3 @@ local comp = {
}
end,
}
-
-return comp
diff --git a/lua/overseer/component/open_output.lua b/lua/overseer/component/open_output.lua
index 92f83584..9fdd3c43 100644
--- a/lua/overseer/component/open_output.lua
+++ b/lua/overseer/component/open_output.lua
@@ -23,7 +23,7 @@ local function open_output(task, direction, focus)
end
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "Open task output",
params = {
on_start = {
@@ -110,5 +110,3 @@ local comp = {
return methods
end,
}
-
-return comp
diff --git a/lua/overseer/component/restart_on_save.lua b/lua/overseer/component/restart_on_save.lua
index 3d01f10e..64319a27 100644
--- a/lua/overseer/component/restart_on_save.lua
+++ b/lua/overseer/component/restart_on_save.lua
@@ -2,7 +2,7 @@ local files = require("overseer.files")
local log = require("overseer.log")
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "Restart on any buffer :write",
params = {
paths = {
@@ -31,15 +31,13 @@ local comp = {
long_desc = "'autocmd' will set autocmds on BufWritePost. 'uv' will use a libuv file watcher (recursive watching may not be supported on all platforms).",
},
interrupt = {
- desc = "Interrupt running tasks",
+ desc = "Interrupt running tasks. If false, will wait for task to complete before restarting",
type = "boolean",
default = true,
},
},
constructor = function(opts)
- vim.validate({
- delay = { opts.delay, "n" },
- })
+ vim.validate("delay", opts.delay, "number")
local function is_watching_file(path)
if not opts.paths then
@@ -97,13 +95,13 @@ local comp = {
})
elseif opts.mode == "uv" then
for _, path in ipairs(opts.paths) do
- local fs_event = assert(vim.loop.new_fs_event())
+ local fs_event = assert(vim.uv.new_fs_event())
fs_event:start(
path,
{ recursive = true },
vim.schedule_wrap(function(err, filename, events)
if err then
- log:warn("Overseer[restart_on_save] watch error: %s", err)
+ log.warn("Overseer[restart_on_save] watch error: %s", err)
else
trigger_restart(task)
end
@@ -140,5 +138,3 @@ local comp = {
}
end,
}
-
-return comp
diff --git a/lua/overseer/component/run_after.lua b/lua/overseer/component/run_after.lua
index d84f2aa3..1f788362 100644
--- a/lua/overseer/component/run_after.lua
+++ b/lua/overseer/component/run_after.lua
@@ -5,14 +5,23 @@ local util = require("overseer.util")
local STATUS = constants.STATUS
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "Run other tasks after this task completes",
params = {
+ tasks = {
+ desc = "Names of dependency task templates",
+ long_desc = 'This can be a list of strings (template names, e.g. "cargo build"), tables (template name with params, e.g. {"mytask", foo = "bar"}), or tables (raw task params, e.g. {cmd = "sleep 10"})',
+ -- TODO Can't input dependencies WITH params in the task launcher b/c the type is too complex
+ type = "list",
+ optional = true,
+ },
task_names = {
+ deprecated = true,
desc = "Names of dependency task templates",
- long_desc = 'This can be a list of strings (template names, e.g. {"cargo build"}), tables (name with params, e.g. {"shell", cmd = "sleep 10"}), or tables (raw task params, e.g. {cmd = "sleep 10"})',
+ long_desc = 'This can be a list of strings (template names, e.g. "cargo build"), tables (template name with params, e.g. {"mytask", foo = "bar"}), or tables (raw task params, e.g. {cmd = "sleep 10"})',
-- TODO Can't input dependencies WITH params in the task launcher b/c the type is too complex
type = "list",
+ optional = true,
},
statuses = {
desc = "Only run successive tasks if the final status is in this list",
@@ -38,7 +47,7 @@ local comp = {
if not vim.tbl_contains(params.statuses, task.status) then
return
end
- for i, name_or_config in ipairs(params.task_names) do
+ for i, name_or_config in ipairs(params.tasks or params.task_names or {}) do
local task_id = self.task_lookup[i]
local after_task = task_id and task_list.get(task_id)
if after_task then
@@ -49,7 +58,7 @@ local comp = {
else
util.run_template_or_task(name_or_config, function(new_task)
if not new_task then
- log:error(
+ log.error(
"Task(%s)[run_after] could not find template %s",
task.name,
name_or_config
@@ -62,9 +71,7 @@ local comp = {
self.task_lookup[i] = new_task.id
table.insert(self.all_tasks, new_task.id)
end
- -- Don't include after tasks when saving to bundle.
- -- We will re-create them when this task runs again
- new_task:set_include_in_bundle(false)
+ new_task.ephemeral = true
new_task:start()
end)
end
@@ -91,5 +98,3 @@ local comp = {
}
end,
}
-
-return comp
diff --git a/lua/overseer/component/timeout.lua b/lua/overseer/component/timeout.lua
index aa1bfaad..959287fc 100644
--- a/lua/overseer/component/timeout.lua
+++ b/lua/overseer/component/timeout.lua
@@ -1,5 +1,5 @@
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "Cancel task if it exceeds a timeout",
params = {
timeout = {
@@ -13,14 +13,12 @@ local comp = {
},
constructor = function(opts)
opts = opts or {}
- vim.validate({
- timeout = { opts.timeout, "n" },
- })
+ vim.validate("timeout", opts.timeout, "number")
return {
timer = nil,
canceled = false,
on_start = function(self, task)
- self.timer = vim.loop.new_timer()
+ self.timer = vim.uv.new_timer()
self.timer:start(
1000 * opts.timeout,
0,
@@ -42,14 +40,6 @@ local comp = {
self.timer = nil
end
end,
- render = function(self, task, lines, highlights, detail)
- if self.canceled then
- table.insert(lines, "Task timed out")
- table.insert(highlights, { "DiagnosticWarn", #lines, 0, -1 })
- end
- end,
}
end,
}
-
-return comp
diff --git a/lua/overseer/component/unique.lua b/lua/overseer/component/unique.lua
index b8169947..6290b863 100644
--- a/lua/overseer/component/unique.lua
+++ b/lua/overseer/component/unique.lua
@@ -1,7 +1,7 @@
local task_list = require("overseer.task_list")
local util = require("overseer.util")
---@type overseer.ComponentFileDefinition
-local comp = {
+return {
desc = "Ensure that this task does not have any duplicates",
-- Doesn't make sense for user to add this using a form.
editable = false,
@@ -17,6 +17,11 @@ local comp = {
type = "boolean",
default = true,
},
+ soft = {
+ desc = "Only dispose duplicate tasks if they are completed. Implies replace = true.",
+ type = "boolean",
+ default = false,
+ },
},
constructor = function(params)
return {
@@ -25,11 +30,13 @@ local comp = {
for _, t in ipairs(tasks) do
if t.name == task.name and t ~= task then
if params.replace then
- task:subscribe("on_start", function()
- util.replace_buffer_in_wins(t:get_bufnr(), task:get_bufnr())
- return false
- end)
- t:dispose(true)
+ if t:is_complete() or not params.soft then
+ task:subscribe("on_start", function()
+ util.replace_buffer_in_wins(t:get_bufnr(), task:get_bufnr())
+ return false
+ end)
+ t:dispose(true)
+ end
else
task:dispose(true)
t:restart(params.restart_interrupts)
@@ -41,5 +48,3 @@ local comp = {
}
end,
}
-
-return comp
diff --git a/lua/overseer/config.lua b/lua/overseer/config.lua
index fc9057ca..fc2e3733 100644
--- a/lua/overseer/config.lua
+++ b/lua/overseer/config.lua
@@ -1,154 +1,91 @@
local default_config = {
- -- Default task strategy
- strategy = "terminal",
- -- Template modules to load
- templates = { "builtin" },
- -- Directories where overseer will look for template definitions (relative to rtp)
- template_dirs = { "overseer.template" },
- -- When true, tries to detect a green color from your colorscheme to use for success highlight
- auto_detect_success_color = true,
-- Patch nvim-dap to support preLaunchTask and postDebugTask
dap = true,
+ -- Configure the task output buffer and window
+ output = {
+ -- Use a terminal buffer to display output. If false, a normal buffer is used
+ use_terminal = true,
+ -- If true, don't clear the buffer when a task restarts
+ preserve_output = false,
+ },
-- Configure the task list
task_list = {
- -- Default detail level for tasks. Can be 1-3.
- default_detail = 1,
+ -- Default direction. Can be "left", "right", or "bottom"
+ direction = "bottom",
-- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
-- min_width and max_width can be a single value or a list of mixed integer/float types.
-- max_width = {100, 0.2} means "the lesser of 100 columns or 20% of total"
max_width = { 100, 0.2 },
-- min_width = {40, 0.1} means "the greater of 40 columns or 10% of total"
min_width = { 40, 0.1 },
- -- optionally define an integer/float for the exact width of the task list
- width = nil,
- max_height = { 20, 0.1 },
+ max_height = { 20, 0.2 },
min_height = 8,
- height = nil,
-- String that separates tasks
- separator = "────────────────────────────────────────",
- -- Default direction. Can be "left", "right", or "bottom"
- direction = "bottom",
+ separator = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
+ -- Indentation for child tasks
+ child_indent = { "┃ ", "┣━", "┗━" },
+ -- Function that renders tasks. See lua/overseer/render.lua for built-in options
+ -- and for useful functions if you want to build your own.
+ render = function(task)
+ return require("overseer.render").format_standard(task)
+ end,
+ -- The sort function for tasks
+ sort = function(a, b)
+ return require("overseer.task_list").default_sort(a, b)
+ end,
-- Set keymap to false to remove default behavior
-- You can add custom keymaps here as well (anything vim.keymap.set accepts)
- bindings = {
- ["?"] = "ShowHelp",
- ["g?"] = "ShowHelp",
- [""] = "RunAction",
- [""] = "Edit",
- ["o"] = "Open",
- [""] = "OpenVsplit",
- [""] = "OpenSplit",
- [""] = "OpenFloat",
- [""] = "OpenQuickFix",
- ["p"] = "TogglePreview",
- [""] = "IncreaseDetail",
- [""] = "DecreaseDetail",
- ["L"] = "IncreaseAllDetail",
- ["H"] = "DecreaseAllDetail",
- ["["] = "DecreaseWidth",
- ["]"] = "IncreaseWidth",
- ["{"] = "PrevTask",
- ["}"] = "NextTask",
- [""] = "ScrollOutputUp",
- [""] = "ScrollOutputDown",
- ["q"] = "Close",
+ keymaps = {
+ ["?"] = "keymap.show_help",
+ ["g?"] = "keymap.show_help",
+ [""] = "keymap.run_action",
+ ["dd"] = { "keymap.run_action", opts = { action = "dispose" }, desc = "Dispose task" },
+ [""] = { "keymap.run_action", opts = { action = "edit" }, desc = "Edit task" },
+ ["o"] = "keymap.open",
+ [""] = { "keymap.open", opts = { dir = "vsplit" }, desc = "Open task output in vsplit" },
+ [""] = { "keymap.open", opts = { dir = "split" }, desc = "Open task output in split" },
+ [""] = { "keymap.open", opts = { dir = "tab" }, desc = "Open task output in tab" },
+ [""] = { "keymap.open", opts = { dir = "float" }, desc = "Open task output in float" },
+ [""] = {
+ "keymap.run_action",
+ opts = { action = "open output in quickfix" },
+ desc = "Open task output in the quickfix",
+ },
+ ["p"] = "keymap.toggle_preview",
+ ["{"] = "keymap.prev_task",
+ ["}"] = "keymap.next_task",
+ [""] = "keymap.scroll_output_up",
+ [""] = "keymap.scroll_output_down",
+ ["g."] = "keymap.toggle_show_wrapped",
+ ["q"] = { "close", desc = "Close task list" },
},
},
- -- See :help overseer-actions
+ -- Custom actions for tasks. See :help overseer-actions
actions = {},
-- Configure the floating window used for task templates that require input
-- and the floating window used for editing tasks
form = {
- border = "rounded",
zindex = 40,
-- Dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
-- min_X and max_X can be a single value or a list of mixed integer/float types.
min_width = 80,
max_width = 0.9,
- width = nil,
min_height = 10,
max_height = 0.9,
- height = nil,
- -- Set any window options here (e.g. winhighlight)
- win_opts = {
- winblend = 0,
- },
- },
- task_launcher = {
- -- Set keymap to false to remove default behavior
- -- You can add custom keymaps here as well (anything vim.keymap.set accepts)
- bindings = {
- i = {
- [""] = "Submit",
- [""] = "Cancel",
- },
- n = {
- [""] = "Submit",
- [""] = "Submit",
- ["q"] = "Cancel",
- ["?"] = "ShowHelp",
- },
- },
- },
- task_editor = {
- -- Set keymap to false to remove default behavior
- -- You can add custom keymaps here as well (anything vim.keymap.set accepts)
- bindings = {
- i = {
- [""] = "NextOrSubmit",
- [""] = "Submit",
- [""] = "Next",
- [""] = "Prev",
- [""] = "Cancel",
- },
- n = {
- [""] = "NextOrSubmit",
- [""] = "Submit",
- [""] = "Next",
- [""] = "Prev",
- ["q"] = "Cancel",
- ["?"] = "ShowHelp",
- },
- },
- },
- -- Configure the floating window used for confirmation prompts
- confirm = {
- border = "rounded",
- zindex = 40,
- -- Dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
- -- min_X and max_X can be a single value or a list of mixed integer/float types.
- min_width = 20,
- max_width = 0.5,
- width = nil,
- min_height = 6,
- max_height = 0.9,
- height = nil,
-- Set any window options here (e.g. winhighlight)
- win_opts = {
- winblend = 0,
- },
+ win_opts = {},
},
- -- Configuration for task floating windows
+ -- Configuration for task floating output windows
task_win = {
-- How much space to leave around the floating window
padding = 2,
- border = "rounded",
-- Set any window options here (e.g. winhighlight)
- win_opts = {
- winblend = 0,
- },
- },
- -- Configuration for mapping help floating windows
- help_win = {
- border = "rounded",
win_opts = {},
},
-- Aliases for bundles of components. Redefine the builtins, or create your own.
component_aliases = {
-- Most tasks are initialized with the default components
default = {
- { "display_duration", detail_level = 2 },
- "on_output_summarize",
"on_exit_set_status",
"on_complete_notify",
{ "on_complete_dispose", require_view = { "SUCCESS", "FAILURE" } },
@@ -158,106 +95,168 @@ local default_config = {
"default",
"on_result_diagnostics",
},
- },
- bundles = {
- -- When saving a bundle with OverseerSaveBundle or save_task_bundle(), filter the tasks with
- -- these options (passed to list_tasks())
- save_task_opts = {
- bundleable = true,
+ -- Tasks created from experimental_wrap_builtins
+ default_builtin = {
+ "on_exit_set_status",
+ "on_complete_dispose",
+ { "unique", soft = true },
},
- -- Autostart tasks when they are loaded from a bundle
- autostart_on_load = true,
},
- -- A list of components to preload on setup.
- -- Only matters if you want them to show up in the task editor.
- preload_components = {},
- -- Controls when the parameter prompt is shown when running a template
- -- always Show when template has any params
- -- missing Show when template has any params not explicitly passed in
- -- allow Only show when a required param is missing
- -- avoid Only show when a required param with no default value is missing
- -- never Never show prompt (error if required param missing)
- default_template_prompt = "allow",
- -- For template providers, how long to wait (in ms) before timing out.
- -- Set to 0 to disable timeouts.
- template_timeout = 3000,
+ -- List of other directories to search for task templates.
+ -- This will search under the runtimepath, so for example
+ -- "foo/bar" will search "/lua/foo/bar/*"
+ template_dirs = {},
+ -- For template providers, how long to wait before timing out.
+ -- Set to 0 to wait forever.
+ template_timeout_ms = 3000,
-- Cache template provider results if the provider takes longer than this to run.
- -- Time is in ms. Set to 0 to disable caching.
- template_cache_threshold = 100,
- -- Configure where the logs go and what level to use
- -- Types are "echo", "notify", and "file"
- log = {
- {
- type = "echo",
- level = vim.log.levels.WARN,
- },
- {
- type = "file",
- filename = "overseer.log",
- level = vim.log.levels.WARN,
- },
+ -- Set to 0 to disable caching.
+ template_cache_threshold_ms = 200,
+ log_level = vim.log.levels.WARN,
+ -- Overseer can wrap any call to vim.system and vim.fn.jobstart as a task.
+ experimental_wrap_builtins = {
+ enabled = false,
+ condition = function(cmd, caller, opts)
+ return true
+ end,
},
}
-local M = vim.deepcopy(default_config)
-
-local function merge_actions(default_actions, user_actions)
- local actions = {}
- for k, v in pairs(default_actions) do
- actions[k] = v
- end
- for k, v in pairs(user_actions or {}) do
- if not v then
- actions[k] = nil
- else
- actions[k] = v
- end
- end
- return actions
-end
-
----If user creates a mapping for an action, remove the default mapping to that action
----(unless they explicitly specify that key as well)
----@param user_conf overseer.Config
-local function remove_binding_conflicts(user_conf)
- for key, configval in pairs(user_conf) do
- if type(configval) == "table" and configval.bindings then
- local orig_bindings = default_config[key].bindings
- local rev = {}
- -- Make a reverse lookup of shortcut-to-key
- -- e.g. ["Open"] = "o"
- for k, v in pairs(orig_bindings) do
- rev[v] = k
- end
- for k, v in pairs(configval.bindings) do
- -- If the user is choosing to map a command to a different key, remove the original default
- -- map (e.g. if {"u" = "Open"}, then set {"o" = false})
- if rev[v] and rev[v] ~= k and not configval.bindings[rev[v]] then
- configval.bindings[rev[v]] = false
- end
- end
- end
- end
-end
+local M = {}
----@param opts? overseer.Config
+local has_setup = false
+---@param opts? overseer.SetupOpts
M.setup = function(opts)
- local component = require("overseer.component")
- local log = require("overseer.log")
+ has_setup = true
opts = opts or {}
- remove_binding_conflicts(opts)
+
local newconf = vim.tbl_deep_extend("force", default_config, opts)
for k, v in pairs(newconf) do
M[k] = v
end
- log.set_root(log.new({ handlers = M.log }))
-
- M.actions = merge_actions(require("overseer.task_list.actions"), newconf.actions)
+ if opts.task_list and opts.task_list.keymaps then
+ -- Handle keymap overrides in a case-insensitive way
+ local case_map = {}
+ for k in pairs(default_config.task_list.keymaps) do
+ case_map[k:lower()] = k
+ end
+ newconf.task_list.keymaps = vim.deepcopy(default_config.task_list.keymaps)
+ -- We don't want to deep merge the keymaps, we want any keymap defined by the user to override
+ -- everything about the default.
+ for k, v in pairs(opts.task_list.keymaps) do
+ k = case_map[k:lower()] or k
+ if v then
+ newconf.task_list.keymaps[k] = v
+ else
+ newconf.task_list.keymaps[k] = nil
+ end
+ end
+ end
- for k, v in pairs(M.component_aliases) do
- component.alias(k, v)
+ for i, dir in ipairs(M.template_dirs) do
+ -- for backwards compatibility, we used to allow module paths
+ M.template_dirs[i] = dir:gsub("%.", "/")
end
end
+---@class (exact) overseer.Config
+---@field setup fun(opts: overseer.SetupOpts)
+---@field dap boolean
+---@field log_level integer
+---@field experimental_wrap_builtins overseer.ConfigWrapBuiltins
+---@field output overseer.ConfigOutput
+---@field task_list overseer.ConfigTaskList
+---@field actions table See :help overseer-actions
+---@field form overseer.ConfigFloatWin
+---@field task_win overseer.ConfigTaskWin
+---@field component_aliases table Aliases for bundles of components. Redefine the builtins, or create your own.
+---@field template_dirs string[] List of other directories to search for task templates.
+---@field template_timeout_ms? integer For template providers, how long to wait (in ms) before timing out. Set to 0 to disable timeouts.
+---@field template_cache_threshold_ms? integer Cache template provider results if the provider takes longer than this to run. Time is in ms. Set to 0 to disable caching.
+
+---@class (exact) overseer.SetupOpts
+---@field dap? boolean Patch nvim-dap to support preLaunchTask and postDebugTask
+---@field log_level? integer Log level
+---@field experimental_wrap_builtins? overseer.SetupConfigWrapBuiltins
+---@field output? overseer.SetupConfigOutput
+---@field task_list? overseer.SetupConfigTaskList
+---@field actions? table See :help overseer-actions
+---@field form? overseer.SetupConfigFloatWin
+---@field task_win? overseer.SetupConfigTaskWin
+---@field component_aliases? table Aliases for bundles of components. Redefine the builtins, or create your own.
+---@field template_dirs? string[] List of other directories to search for task templates.
+---@field template_timeout_ms? integer For template providers, how long to wait (in ms) before timing out. Set to 0 to disable timeouts.
+---@field template_cache_threshold_ms? integer Cache template provider results if the provider takes longer than this to run. Time is in ms. Set to 0 to disable caching.
+
+---@class (exact) overseer.ConfigWrapBuiltins
+---@field enabled boolean overseer will hook vim.system and vim.fn.jobstart and display those as tasks
+---@field condition fun(cmd: string|string[], caller: overseer.Caller, opts?: table): boolean callback to determine if overseer should create a task for this jobstart/system
+
+---@class (exact) overseer.SetupConfigWrapBuiltins
+---@field enabled? boolean overseer will hook vim.system and vim.fn.jobstart and display those as tasks
+---@field condition? fun(cmd: string|string[], caller: overseer.Caller, opts?: table): boolean callback to determine if overseer should create a task for this jobstart/system
+
+---@class (exact) overseer.ConfigOutput
+---@field preserve_output? boolean
+---@field use_terminal? boolean
+
+---@class (exact) overseer.SetupConfigOutput
+---@field preserve_output? boolean Use a terminal buffer to display output. If false, a normal buffer is used.
+---@field use_terminal? boolean If true, don't clear the buffer when a task restarts
+
+---@class (exact) overseer.ConfigTaskList : overseer.LayoutOpts
+---@field direction "left"|"right"|"bottom"
+---@field separator string String that separates tasks
+---@field child_indent {[1]: string, [2]: string, [3]: string}
+---@field render fun(task: overseer.Task): overseer.TextChunk[][] Function that renders tasks
+---@field sort fun(a: overseer.Task, b: overseer.Task): boolean Function that sorts tasks
+---@field keymaps table Set keymap to false to remove default behavior
+
+---@class (exact) overseer.SetupConfigTaskList
+---@field direction? "left"|"right"|"bottom" Direction to open task list (default "bottom")
+---@field max_width? number|number[] Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). min_width and max_width can be a single value or a list of mixed integer/float types. max_width = {100, 0.2} means "the lesser of 100 columns or 20% of total"
+---@field min_width? number|number[] min_width = {40, 0.1} means "the greater of 40 columns or 10% of total"
+---@field max_height? number|number[]
+---@field min_height? number|number[]
+---@field separator? string String that separates tasks
+---@field child_indent? {[1]: string, [2]: string, [3]: string}
+---@field render? fun(task: overseer.Task): overseer.TextChunk[][] Function that renders tasks
+---@field sort? fun(a: overseer.Task, b: overseer.Task): boolean Function that sorts tasks
+---@field keymaps? table Set keymap to false to remove default behavior
+
+---@class (exact) overseer.ConfigFloatWin : overseer.LayoutOpts
+---@field zindex integer
+---@field win_opts table
+
+---@class (exact) overseer.SetupConfigFloatWin
+---@field zindex? integer
+---@field min_width? number|number[]
+---@field max_width? number|number[]
+---@field min_height? number|number[]
+---@field max_height? number|number[]
+---@field win_opts? table
+
+---@class (exact) overseer.ConfigTaskWin
+---@field padding integer
+---@field zindex? integer
+---@field win_opts table
+
+---@class (exact) overseer.SetupConfigTaskWin
+---@field padding? integer How much space to leave around the floating window
+---@field zindex? integer
+---@field win_opts? table Set any window options here (e.g. winhighlight)
+
+setmetatable(M, {
+ -- If the user hasn't called setup() yet, make sure we correctly set up the config object so there
+ -- aren't random crashes.
+ __index = function(self, key)
+ if not has_setup then
+ M.setup()
+ end
+ return rawget(self, key)
+ end,
+})
+
+---@cast M overseer.Config
return M
diff --git a/lua/overseer/confirm.lua b/lua/overseer/confirm.lua
deleted file mode 100644
index d0c620ec..00000000
--- a/lua/overseer/confirm.lua
+++ /dev/null
@@ -1,147 +0,0 @@
-local config = require("overseer.config")
-local layout = require("overseer.layout")
-
-return function(opts, callback)
- vim.validate({
- message = { opts.message, "s" },
- choices = { opts.choices, "t", true },
- default = { opts.default, "n", true },
- type = { opts.type, "s", true },
- callback = { callback, "f" },
- })
- if not opts.choices then
- opts.choices = { "&OK" }
- end
- if not opts.default then
- opts.default = 1
- end
- -- TODO this doesn't do anything yet
- if not opts.type then
- opts.type = "G"
- else
- opts.type = string.sub(opts.type, 1, 1)
- end
-
- local bufnr = vim.api.nvim_create_buf(false, true)
- vim.bo[bufnr].buftype = "nofile"
- vim.bo[bufnr].bufhidden = "wipe"
- vim.bo[bufnr].buflisted = false
- vim.bo[bufnr].swapfile = false
- local winid
-
- local function choose(idx)
- local cb = callback
- callback = function(_) end
- if winid then
- vim.api.nvim_win_close(winid, true)
- end
- cb(idx)
- end
- local function cancel()
- choose(0)
- end
-
- local clean_choices = {}
- local choice_shortcut_idx = {}
- for i, choice in ipairs(opts.choices) do
- local idx = choice:find("&")
- local key
- if idx and idx < string.len(choice) then
- table.insert(clean_choices, choice:sub(1, idx - 1) .. choice:sub(idx + 1))
- key = choice:sub(idx + 1, idx + 1)
- table.insert(choice_shortcut_idx, idx)
- else
- key = choice:sub(1, 1)
- table.insert(clean_choices, choice)
- table.insert(choice_shortcut_idx, 1)
- end
- vim.keymap.set("n", key:lower(), function()
- choose(i)
- end, { buffer = bufnr })
- vim.keymap.set("n", key:upper(), function()
- choose(i)
- end, { buffer = bufnr })
- end
- vim.keymap.set("n", "", cancel, { buffer = bufnr })
- vim.keymap.set("n", "", cancel, { buffer = bufnr })
-
- local lines = vim.split(opts.message, "\n")
- local highlights = {}
- table.insert(lines, "")
-
- -- Calculate the width of the choices if they are on a single line
- local choices_width = 0
- for _, choice in ipairs(clean_choices) do
- choices_width = choices_width + vim.api.nvim_strwidth(choice)
- end
- -- Make sure to account for spacing
- choices_width = choices_width + #clean_choices - 1
-
- local desired_width = choices_width
- for _, line in ipairs(lines) do
- local len = string.len(line)
- if len > desired_width then
- desired_width = len
- end
- end
-
- local width = layout.calculate_width(desired_width, config.confirm)
-
- if width < choices_width then
- -- Render one choice per line
- for i, choice in ipairs(clean_choices) do
- table.insert(lines, choice)
- table.insert(highlights, { "Keyword", #lines, choice_shortcut_idx[i] - 1 })
- end
- else
- -- Render all choices on a single line
- local extra_spacing = width - choices_width
- local line = ""
- local num_dividers = #clean_choices - 1
- for i, choice in ipairs(clean_choices) do
- if i > 1 then
- line = line .. " " .. string.rep(" ", math.floor(extra_spacing / num_dividers))
- if extra_spacing % num_dividers >= i then
- line = line .. " "
- end
- end
- local col_start = line:len() - 1
- line = line .. choice
- table.insert(highlights, { "Keyword", #lines + 1, col_start + choice_shortcut_idx[i] })
- end
- table.insert(lines, line)
- end
-
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
- local ns = vim.api.nvim_create_namespace("confirm")
- for _, hl in ipairs(highlights) do
- local group, lnum, col_start, col_end = unpack(hl)
- if not col_end then
- col_end = col_start + 1
- end
- vim.api.nvim_buf_add_highlight(bufnr, ns, group, lnum - 1, col_start, col_end)
- end
-
- local height = layout.calculate_height(#lines, config.confirm)
- winid = vim.api.nvim_open_win(bufnr, true, {
- relative = "editor",
- border = config.confirm.border,
- zindex = config.confirm.zindex,
- style = "minimal",
- width = width,
- height = height,
- col = math.floor((layout.get_editor_width() - width) / 2),
- row = math.floor((layout.get_editor_height() - height) / 2),
- })
- for k, v in pairs(config.confirm.win_opts) do
- vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid })
- end
-
- vim.api.nvim_create_autocmd("BufLeave", {
- buffer = bufnr,
- callback = cancel,
- once = true,
- nested = true,
- })
- vim.bo[bufnr].modifiable = false
-end
diff --git a/lua/overseer/dap.lua b/lua/overseer/dap.lua
index 9bf3ff69..359b8ddc 100644
--- a/lua/overseer/dap.lua
+++ b/lua/overseer/dap.lua
@@ -1,7 +1,7 @@
local constants = require("overseer.constants")
local dap = require("dap")
local log = require("overseer.log")
-local vscode = require("overseer.template.vscode")
+local vscode = require("overseer.vscode")
local STATUS = constants.STATUS
local TAG = constants.TAG
local M = {}
@@ -17,7 +17,7 @@ local function get_task(name, config, cb)
else
args.name = name
end
- require("overseer").run_template(args, cb)
+ require("overseer").run_task(args, cb)
end
M.listener = function(config)
@@ -28,10 +28,10 @@ M.listener = function(config)
if config.postDebugTask then
dap.listeners.after.event_terminated.overseer = function()
- log:debug("Running DAP postDebugTask %s", config.postDebugTask)
+ log.debug("Running DAP postDebugTask %s", config.postDebugTask)
get_task(config.postDebugTask, config, function(task, err)
if err then
- log:error("Could not run postDebugTask %s", config.postDebugTask)
+ log.error("Could not run postDebugTask %s", config.postDebugTask)
elseif task then
task:start()
end
@@ -40,11 +40,11 @@ M.listener = function(config)
end
if config.preLaunchTask then
- log:debug("Running DAP preLaunchTask %s", config.preLaunchTask)
+ log.debug("Running DAP preLaunchTask %s", config.preLaunchTask)
local co = coroutine.running()
get_task(config.preLaunchTask, config, function(task, err)
if not task then
- log:error("Could not run preLaunchTask %s: %s", config.preLaunchTask, err)
+ log.error("Could not run preLaunchTask %s: %s", config.preLaunchTask, err)
return
end
diff --git a/lua/overseer/files.lua b/lua/overseer/files.lua
index 223d9f1a..300228eb 100644
--- a/lua/overseer/files.lua
+++ b/lua/overseer/files.lua
@@ -1,10 +1,10 @@
local M = {}
---@type boolean
-M.is_windows = vim.loop.os_uname().version:match("Windows")
+M.is_windows = vim.uv.os_uname().version:match("Windows")
---@type boolean
-M.is_mac = vim.loop.os_uname().sysname == "Darwin"
+M.is_mac = vim.uv.os_uname().sysname == "Darwin"
---@type string
M.sep = M.is_windows and "\\" or "/"
@@ -23,21 +23,10 @@ end
---@param filepath string
---@return boolean
M.exists = function(filepath)
- local stat = vim.loop.fs_stat(filepath)
+ local stat = vim.uv.fs_stat(filepath)
return stat ~= nil and stat.type ~= nil
end
----@return string
-M.join = function(...)
- local joined = table.concat({ ... }, M.sep)
- if M.is_windows then
- joined = joined:gsub("\\\\+", "\\")
- else
- joined = joined:gsub("//+", "/")
- end
- return joined
-end
-
M.is_absolute = function(path)
if M.is_windows then
return path:lower():match("^%a:")
@@ -105,10 +94,10 @@ M.read_file = function(filepath)
if not M.exists(filepath) then
return nil
end
- local fd = assert(vim.loop.fs_open(filepath, "r", 420)) -- 0644
- local stat = assert(vim.loop.fs_fstat(fd))
- local content = vim.loop.fs_read(fd, stat.size)
- vim.loop.fs_close(fd)
+ local fd = assert(vim.uv.fs_open(filepath, "r", 420)) -- 0644
+ local stat = assert(vim.uv.fs_fstat(fd))
+ local content = vim.uv.fs_read(fd, stat.size)
+ vim.uv.fs_close(fd)
return content
end
@@ -137,9 +126,9 @@ end
---@return string[]
M.list_files = function(dir)
---@diagnostic disable-next-line: param-type-mismatch
- local fd = vim.loop.fs_opendir(dir, nil, 32)
+ local fd = vim.uv.fs_opendir(dir, nil, 32)
---@diagnostic disable-next-line: param-type-mismatch
- local entries = vim.loop.fs_readdir(fd)
+ local entries = vim.uv.fs_readdir(fd)
local ret = {}
while entries do
for _, entry in ipairs(entries) do
@@ -148,10 +137,10 @@ M.list_files = function(dir)
end
end
---@diagnostic disable-next-line: param-type-mismatch
- entries = vim.loop.fs_readdir(fd)
+ entries = vim.uv.fs_readdir(fd)
end
---@diagnostic disable-next-line: param-type-mismatch
- vim.loop.fs_closedir(fd)
+ vim.uv.fs_closedir(fd)
return ret
end
@@ -166,7 +155,7 @@ M.mkdir = function(dirname, perms)
if not M.exists(parent) then
M.mkdir(parent)
end
- vim.loop.fs_mkdir(dirname, perms)
+ vim.uv.fs_mkdir(dirname, perms)
end
end
@@ -174,15 +163,15 @@ end
---@param contents string
M.write_file = function(filename, contents)
M.mkdir(vim.fn.fnamemodify(filename, ":p:h"))
- local fd = assert(vim.loop.fs_open(filename, "w", 420)) -- 0644
- vim.loop.fs_write(fd, contents)
- vim.loop.fs_close(fd)
+ local fd = assert(vim.uv.fs_open(filename, "w", 420)) -- 0644
+ vim.uv.fs_write(fd, contents)
+ vim.uv.fs_close(fd)
end
---@param filename string
M.delete_file = function(filename)
if M.exists(filename) then
- vim.loop.fs_unlink(filename)
+ vim.uv.fs_unlink(filename)
return true
end
end
diff --git a/lua/overseer/form/init.lua b/lua/overseer/form/init.lua
index 28822e7e..cd559b51 100644
--- a/lua/overseer/form/init.lua
+++ b/lua/overseer/form/init.lua
@@ -1,66 +1,15 @@
-local binding_util = require("overseer.binding_util")
-local config = require("overseer.config")
local form_utils = require("overseer.form.utils")
local util = require("overseer.util")
local M = {}
-local bindings = {
- {
- desc = "Show default key bindings",
- plug = "OverseerTaskEditor:ShowHelp",
- rhs = function(builder)
- builder.disable_close_on_leave = true
- binding_util.show_bindings("OverseerTaskEditor:")
- end,
- },
- {
- desc = "Move to next form field, or submit the task if on the last field",
- plug = "OverseerTaskEditor:NextOrSubmit",
- rhs = function(builder)
- builder:confirm()
- end,
- },
- {
- desc = "Submit the task",
- plug = "OverseerTaskEditor:Submit",
- rhs = function(builder)
- builder:submit()
- end,
- },
- {
- desc = "Cancel the task",
- plug = "OverseerTaskEditor:Cancel",
- rhs = function(builder)
- builder:cancel()
- end,
- },
- {
- desc = "Move to the next field",
- plug = "OverseerTaskEditor:Next",
- rhs = function(builder)
- builder:next_field()
- end,
- },
- {
- desc = "Move to the previous field",
- plug = "OverseerTaskEditor:Prev",
- rhs = function(builder)
- builder:prev_field()
- end,
- },
-}
-
-local Builder = {}
+local Form = {}
local function line_len(bufnr, lnum)
return string.len(vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1])
end
-local function parse_line(line)
- return line:match("^%*?([^%s]+): ?(.*)$")
-end
-function Builder.new(title, schema, params, callback)
+function Form.new(title, schema, params, callback)
-- Filter out the opaque types
local keys = vim.tbl_filter(function(key)
return schema[key].type ~= "opaque"
@@ -100,19 +49,20 @@ function Builder.new(title, schema, params, callback)
vim.bo[bufnr].swapfile = false
vim.bo[bufnr].bufhidden = "wipe"
vim.bo[bufnr].buftype = "acwrite"
- vim.api.nvim_buf_set_name(bufnr, "Overseer task builder")
+ vim.api.nvim_buf_set_name(bufnr, "Overseer form")
- local autocmds = {}
- local builder
+ local form
local cleanup, layout = form_utils.open_form_win(bufnr, {
- autocmds = autocmds,
on_resize = function()
- builder:render()
+ form:render()
end,
get_preferred_dim = function()
local max_len = 1
for k, v in pairs(schema) do
- local len = string.len(form_utils.render_field(v, " ", k, params[k]))
+ local len = string.len(k .. tostring(form_utils.render_value(v, params[k]))) + 2
+ if v.required then
+ len = len + 1
+ end
if v.desc then
len = len + 1 + string.len(v.desc)
end
@@ -123,148 +73,146 @@ function Builder.new(title, schema, params, callback)
return max_len, #keys + 1
end,
})
- table.insert(
- autocmds,
- vim.api.nvim_create_autocmd("BufEnter", {
- desc = "Reset disable_close_on_leave",
- buffer = bufnr,
- nested = true,
- callback = function()
- builder.disable_close_on_leave = false
- end,
- })
- )
+ vim.api.nvim_create_autocmd("BufEnter", {
+ desc = "Reset disable_close_on_leave",
+ buffer = bufnr,
+ nested = true,
+ callback = function()
+ form.disable_close_on_leave = false
+ end,
+ })
vim.bo[bufnr].filetype = "OverseerForm"
+ local called_callback = false
+ local function cb(...)
+ if not called_callback then
+ callback(...)
+ called_callback = true
+ end
+ end
- builder = setmetatable({
+ form = setmetatable({
disable_close_on_leave = false,
cur_line = nil,
title = title,
schema_keys = keys,
schema = schema,
params = params,
- callback = callback,
+ callback = cb,
cleanup = cleanup,
layout = layout,
- autocmds = autocmds,
bufnr = bufnr,
fields_focused = {},
fields_ever_focused = {},
+ ext_id_to_schema_field_name = {},
ever_submitted = false,
- }, { __index = Builder })
- builder:init_autocmds()
- builder:init_keymaps()
+ }, { __index = Form })
+ form:init_autocmds()
+
+ vim.keymap.set({ "i", "n" }, "", function()
+ form:cancel()
+ end, { buffer = bufnr })
+ vim.keymap.set("n", "q", function()
+ form:cancel()
+ end, { buffer = bufnr })
+ vim.keymap.set("i", "", function()
+ form:confirm()
+ end, { buffer = bufnr })
vim.api.nvim_create_autocmd("BufWriteCmd", {
desc = "Submit on buffer write",
buffer = bufnr,
callback = function()
- builder:submit()
+ form:submit()
end,
})
- return builder
+ return form
end
-function Builder:init_autocmds()
- table.insert(
- self.autocmds,
- vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
- desc = "Update form on change",
- buffer = self.bufnr,
- nested = true,
- callback = function()
- local lnum = vim.api.nvim_win_get_cursor(0)[1]
- local line = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, true)[1]
- self.cur_line = { lnum, line }
- self:parse()
- end,
- })
- )
- table.insert(
- self.autocmds,
- vim.api.nvim_create_autocmd("InsertLeave", {
- desc = "Rerender form",
- buffer = self.bufnr,
- callback = function()
- self:render()
- end,
- })
- )
- table.insert(
- self.autocmds,
- vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, {
- desc = "Update form on move cursor",
- buffer = self.bufnr,
- nested = true,
- callback = function()
- self:on_cursor_move()
- end,
- })
- )
- table.insert(
- self.autocmds,
- vim.api.nvim_create_autocmd("BufLeave", {
- desc = "Close float on BufLeave",
- buffer = self.bufnr,
- nested = true,
- callback = function()
- if not self.disable_close_on_leave then
- self:cancel()
- end
- end,
- })
- )
-end
-
-function Builder:init_keymaps()
- binding_util.create_plug_bindings(self.bufnr, bindings, self)
- for mode, user_bindings in pairs(config.task_editor.bindings) do
- binding_util.create_bindings_to_plug(self.bufnr, mode, user_bindings, "OverseerTaskEditor:")
- end
-
- -- Some shenanigans to make behave the way we expect
- vim.keymap.set("i", "", function()
- local cur = vim.api.nvim_win_get_cursor(0)
- local line = vim.api.nvim_buf_get_lines(self.bufnr, cur[1] - 1, cur[1], true)[1]
- local name = line:match("^[^%s]+: ")
- if name then
- local rem = string.sub(line, cur[2] + 1)
- vim.api.nvim_buf_set_lines(
- self.bufnr,
- cur[1] - 1,
- cur[1],
- true,
- { string.format("%s%s", name, rem) }
- )
+function Form:init_autocmds()
+ vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
+ desc = "Update form on change",
+ buffer = self.bufnr,
+ nested = true,
+ callback = function()
+ local lnum = vim.api.nvim_win_get_cursor(0)[1]
+ local line = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, true)[1]
+ self.cur_line = { lnum, line }
self:parse()
- vim.api.nvim_win_set_cursor(0, { cur[1], 0 })
- end
- end, { buffer = self.bufnr })
+ vim.bo[self.bufnr].modified = false
+ end,
+ })
+ vim.api.nvim_create_autocmd("InsertLeave", {
+ desc = "Rerender form",
+ buffer = self.bufnr,
+ callback = function()
+ self:render()
+ end,
+ })
+ vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, {
+ desc = "Update form on move cursor",
+ buffer = self.bufnr,
+ nested = true,
+ callback = function()
+ self:on_cursor_move()
+ end,
+ })
+ vim.api.nvim_create_autocmd("BufLeave", {
+ desc = "Close float on BufLeave",
+ buffer = self.bufnr,
+ nested = true,
+ callback = function()
+ if not self.disable_close_on_leave then
+ self:cancel()
+ end
+ end,
+ })
end
-function Builder:render()
+function Form:render()
local title_ns = vim.api.nvim_create_namespace("overseer_title")
vim.api.nvim_buf_clear_namespace(self.bufnr, title_ns, 0, -1)
local lines = { util.align(self.title, vim.api.nvim_win_get_width(0), "center") }
- local highlights = { { "OverseerTask", 1, 0, -1 } }
local extmarks = {}
+ table.insert(extmarks, {
+ 0,
+ 0,
+ { hl_group = "OverseerTask", end_col = #lines[1] },
+ })
+ local extmark_idx_to_name = {}
for _, name in ipairs(self.schema_keys) do
local prefix = self.schema[name].optional and "" or "*"
local schema = self.schema[name]
- table.insert(lines, form_utils.render_field(schema, prefix, name, self.params[name]))
+ local field_hl = "OverseerField"
+ if
+ (self.fields_ever_focused[name] or self.ever_submitted)
+ and not form_utils.validate_field(self.schema[name], self.params[name])
+ then
+ field_hl = "DiagnosticError"
+ end
+ table.insert(extmarks, {
+ #lines,
+ 0,
+ {
+ virt_text = { { prefix, "NormalFloat" }, { name, field_hl }, { ": ", "NormalFloat" } },
+ virt_text_pos = "inline",
+ undo_restore = false,
+ invalidate = true,
+ },
+ })
+ extmark_idx_to_name[#extmarks] = name
+ table.insert(lines, tostring(form_utils.render_value(schema, self.params[name])))
if schema.conceal then
- local start_col = (prefix .. name):len() + 1
- local length = #lines[#lines] - start_col
+ local length = #lines[#lines]
-- Because conceallevel replaces every concealed _block_ with a single character, we have to
-- create 1-width conceal blocks, one for each character
- for i = 1, length do
+ for i = 0, length do
table.insert(extmarks, {
#lines - 1,
- start_col + i,
+ i,
{
- right_gravity = false,
+ undo_restore = false,
strict = false,
conceal = "*",
- end_col = start_col + i + 2,
+ end_col = i + 1,
},
})
end
@@ -276,46 +224,43 @@ function Builder:render()
lines[lnum] = line
end
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, true, lines)
- util.add_highlights(self.bufnr, title_ns, highlights)
- for _, mark in ipairs(extmarks) do
+ self.ext_id_to_schema_field_name = {}
+ for i, mark in ipairs(extmarks) do
local line, col, opts = unpack(mark)
- vim.api.nvim_buf_set_extmark(self.bufnr, title_ns, line, col, opts)
+ local ext_id = vim.api.nvim_buf_set_extmark(self.bufnr, title_ns, line, col, opts)
+ self.ext_id_to_schema_field_name[ext_id] = extmark_idx_to_name[i]
end
self:on_cursor_move()
end
-function Builder:on_cursor_move()
+function Form:on_cursor_move()
local cur = vim.api.nvim_win_get_cursor(0)
if self.cur_line and self.cur_line[1] ~= cur[1] then
self.cur_line = nil
self:render()
return
end
- local original_cur = vim.deepcopy(cur)
local ns = vim.api.nvim_create_namespace("overseer")
vim.api.nvim_buf_clear_namespace(self.bufnr, ns, 0, -1)
local num_lines = vim.api.nvim_buf_line_count(self.bufnr)
-- Top line is title
if cur[1] == 1 and num_lines > 1 then
- cur[1] = 2
+ vim.schedule_wrap(vim.api.nvim_win_set_cursor)(0, { 2, 0 })
+ return
end
local buflines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, true)
+ local lnum_to_field_name = self:get_lnum_to_field_name()
for i, line in ipairs(buflines) do
- local name, text = parse_line(line)
+ local name = lnum_to_field_name[i]
if name and self.schema[name] then
local focused = i == cur[1]
- local name_end = string.len(line) - string.len(text)
- -- Move cursor to input section of field
if focused then
- if cur[2] < name_end then
- cur[2] = name_end
- end
local schema = self.schema[name]
local vtext = {}
if schema.type == "namedEnum" then
- local value = schema.choices[text]
+ local value = schema.choices[line]
if value then
table.insert(vtext, { string.format("[%s] ", value), "Comment" })
end
@@ -341,29 +286,34 @@ function Builder:on_cursor_move()
elseif self.fields_focused[name] then
self.fields_ever_focused[name] = true
end
-
- local group = "OverseerField"
- if
- (self.fields_ever_focused[name] or self.ever_submitted)
- and not form_utils.validate_field(self.schema[name], self.params[name])
- then
- group = "DiagnosticError"
- end
- vim.api.nvim_buf_add_highlight(self.bufnr, ns, group, i - 1, 0, name_end)
end
end
+end
- if cur and (cur[1] ~= original_cur[1] or cur[2] ~= original_cur[2]) then
- vim.api.nvim_win_set_cursor(0, cur)
+---@private
+---@return table
+function Form:get_lnum_to_field_name()
+ local title_ns = vim.api.nvim_create_namespace("overseer_title")
+ local extmarks =
+ vim.api.nvim_buf_get_extmarks(self.bufnr, title_ns, 0, -1, { type = "virt_text" })
+ local lnum_to_field_name = {}
+ for _, extmark in ipairs(extmarks) do
+ local ext_id, row = extmark[1], extmark[2]
+ local field_name = self.ext_id_to_schema_field_name[ext_id]
+ if field_name then
+ lnum_to_field_name[row + 1] = field_name
+ end
end
+ return lnum_to_field_name
end
-function Builder:parse()
+function Form:parse()
local buflines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, true)
- for _, line in ipairs(buflines) do
- local name, text = parse_line(line)
+ local lnum_to_field_name = self:get_lnum_to_field_name()
+ for i, line in ipairs(buflines) do
+ local name = lnum_to_field_name[i]
if name and self.schema[name] then
- local parsed, value = form_utils.parse_value(self.schema[name], text)
+ local parsed, value = form_utils.parse_value(self.schema[name], line)
if parsed then
self.params[name] = value
end
@@ -373,15 +323,19 @@ function Builder:parse()
self:render()
end
-function Builder:cancel()
- self.cleanup()
+function Form:cancel()
self.callback(nil)
+ self.cleanup()
end
-function Builder:submit()
+function Form:submit()
+ local first_submit = not self.ever_submitted
self.ever_submitted = true
for i, name in pairs(self.schema_keys) do
if not form_utils.validate_field(self.schema[name], self.params[name]) then
+ if first_submit then
+ self:render()
+ end
local lnum = i + 1
if vim.api.nvim_win_get_cursor(0)[1] ~= lnum then
vim.api.nvim_win_set_cursor(0, { lnum, line_len(self.bufnr, lnum) })
@@ -391,11 +345,11 @@ function Builder:submit()
return
end
end
- self.cleanup()
self.callback(self.params)
+ self.cleanup()
end
-function Builder:next_field()
+function Form:next_field()
local lnum = vim.api.nvim_win_get_cursor(0)[1]
if lnum == vim.api.nvim_buf_line_count(0) then
return false
@@ -405,7 +359,7 @@ function Builder:next_field()
end
end
-function Builder:prev_field()
+function Form:prev_field()
local lnum = vim.api.nvim_win_get_cursor(0)[1]
if lnum == 1 then
return false
@@ -414,7 +368,7 @@ function Builder:prev_field()
end
end
-function Builder:confirm()
+function Form:confirm()
if not self:next_field() then
self:submit()
end
@@ -431,8 +385,8 @@ M.open = function(title, schema, params, callback)
callback(params)
return
end
- local builder = Builder.new(title, schema, params, callback)
- builder:render()
+ local form = Form.new(title, schema, params, callback)
+ form:render()
vim.defer_fn(function()
vim.cmd([[startinsert!]])
diff --git a/lua/overseer/form/utils.lua b/lua/overseer/form/utils.lua
index ec84068b..61ba966d 100644
--- a/lua/overseer/form/utils.lua
+++ b/lua/overseer/form/utils.lua
@@ -2,14 +2,13 @@ local config = require("overseer.config")
local layout = require("overseer.layout")
local log = require("overseer.log")
local util = require("overseer.util")
----@diagnostic disable-next-line: deprecated
-local islist = vim.islist or vim.tbl_islist
local M = {}
---@alias overseer.Param overseer.StringParam|overseer.BoolParam|overseer.NumberParam|overseer.IntParam|overseer.ListParam|overseer.EnumParam|overseer.NamedEnumParam|overseer.OpaqueParam
---@class overseer.BaseParam
---@field name? string
+---@field deprecated? boolean
---@field desc? string
---@field long_desc? string
---@field order? integer
@@ -70,12 +69,10 @@ M.validate_params = function(params)
if name:match("%s") then
error(string.format("Param '%s' cannot contain whitespace", name))
end
- vim.validate({
- name = { param.name, "s", true },
- desc = { param.desc, "s", true },
- optional = { param.optional, "b", true },
- -- default = any type
- })
+ vim.validate("name", param.name, "string", true)
+ vim.validate("desc", param.desc, "string", true)
+ vim.validate("optional", param.optional, "boolean", true)
+ -- default = any type
local default = default_schema[param.type]
if default then
for k, v in pairs(default) do
@@ -115,16 +112,6 @@ M.render_value = function(schema, value)
return value
end
----@param schema overseer.Param
----@param prefix string
----@param name string
----@param value any
----@return string
-M.render_field = function(schema, prefix, name, value)
- local str_value = M.render_value(schema, value)
- return string.format("%s%s: %s", prefix, name, str_value)
-end
-
---@param schema overseer.Param
---@param value any
---@return boolean
@@ -139,7 +126,7 @@ local function validate_type(schema, value)
elseif ptype == "namedEnum" then
return vim.tbl_contains(vim.tbl_values(schema.choices), value)
elseif ptype == "list" then
- return type(value) == "table" and islist(value)
+ return type(value) == "table" and vim.islist(value)
elseif ptype == "number" then
return type(value) == "number"
elseif ptype == "integer" then
@@ -149,7 +136,7 @@ local function validate_type(schema, value)
elseif ptype == "string" then
return true
else
- log:warn("Unknown param type '%s'", ptype)
+ log.warn("Unknown param type '%s'", ptype)
return false
end
end
@@ -264,14 +251,14 @@ end
local registered_cmp = false
+---@param bufnr integer
+---@param opts? { on_resize?: fun(), get_preferred_dim?: fun(): integer, integer }
+---@return fun() cleanup
+---@return fun() set_layout
M.open_form_win = function(bufnr, opts)
opts = opts or {}
- vim.validate({
- autocmds = { opts.autocmds, "t", true },
- on_resize = { opts.on_resize, "f", true },
- get_preferred_dim = { opts.get_preferred_dim, "f", true },
- })
- opts.autocmds = opts.autocmds or {}
+ vim.validate("on_resize", opts.on_resize, "function", true)
+ vim.validate("get_preferred_dim", opts.get_preferred_dim, "function", true)
local function calc_layout()
local desired_width
local desired_height
@@ -282,7 +269,6 @@ M.open_form_win = function(bufnr, opts)
local height = layout.calculate_height(desired_height, config.form)
local win_opts = {
relative = "editor",
- border = config.form.border,
zindex = config.form.zindex,
width = width,
height = height,
@@ -303,11 +289,18 @@ M.open_form_win = function(bufnr, opts)
end
local function set_layout()
- vim.api.nvim_win_set_config(winid, calc_layout())
+ if vim.api.nvim_win_is_valid(winid) then
+ vim.api.nvim_win_set_config(winid, calc_layout())
+ else
+ return true
+ end
end
local winwidth = vim.api.nvim_win_get_width(winid)
local function on_win_scrolled()
+ if not vim.api.nvim_win_is_valid(winid) then
+ return true
+ end
local new_width = vim.api.nvim_win_get_width(winid)
if winwidth ~= new_width then
winwidth = new_width
@@ -316,45 +309,18 @@ M.open_form_win = function(bufnr, opts)
end
if opts.on_resize then
- table.insert(
- opts.autocmds,
- vim.api.nvim_create_autocmd("WinScrolled", {
- desc = "Rerender on window resize",
- pattern = tostring(winid),
- nested = true,
- callback = on_win_scrolled,
- })
- )
- end
- table.insert(
- opts.autocmds,
- vim.api.nvim_create_autocmd("VimResized", {
- desc = "Rerender on vim resize",
+ vim.api.nvim_create_autocmd("WinScrolled", {
+ desc = "Rerender on window resize",
+ pattern = tostring(winid),
nested = true,
- callback = set_layout,
+ callback = on_win_scrolled,
})
- )
- -- This is a little bit of a hack. We force the cursor to be *after the ': '
- -- of the fields, but if the user enters insert mode with "i", the cursor will
- -- now be before the space. If they type, the parsing will misbehave. So we
- -- detect that and just...nudge them forwards a bit.
- table.insert(
- opts.autocmds,
- vim.api.nvim_create_autocmd("InsertCharPre", {
- desc = "Move cursor to end of line when inserting",
- buffer = bufnr,
- nested = true,
- callback = function()
- local cur = vim.api.nvim_win_get_cursor(0)
- local lnum = cur[1]
- local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1]
- local name = line:match("^[^%s]+: ")
- if name and cur[2] < string.len(name) then
- vim.api.nvim_win_set_cursor(0, { lnum, string.len(name) })
- end
- end,
- })
- )
+ end
+ vim.api.nvim_create_autocmd("VimResized", {
+ desc = "Rerender on vim resize",
+ nested = true,
+ callback = set_layout,
+ })
vim.bo[bufnr].omnifunc = "v:lua.overseer_form_omnifunc"
-- Configure nvim-cmp if installed
@@ -374,9 +340,6 @@ M.open_form_win = function(bufnr, opts)
end
local function cleanup()
- for _, id in ipairs(opts.autocmds) do
- vim.api.nvim_del_autocmd(id)
- end
util.leave_insert()
vim.api.nvim_win_close(winid, true)
end
diff --git a/lua/overseer/health.lua b/lua/overseer/health.lua
new file mode 100644
index 00000000..c999376f
--- /dev/null
+++ b/lua/overseer/health.lua
@@ -0,0 +1,70 @@
+local commands = require("overseer.commands")
+local config = require("overseer.config")
+local log = require("overseer.log")
+
+local M = {}
+
+local level_map = {}
+for k, v in pairs(vim.log.levels) do
+ level_map[v] = k
+end
+
+M.check = function()
+ vim.health.start("overseer.nvim report")
+
+ if vim.fn.has("nvim-0.11") == 0 then
+ vim.health.error("Neovim 0.11 or later is required")
+ end
+ vim.health.info(string.format("Log file: %s", log.get_logfile()))
+ vim.health.info(string.format("Log level: %s", level_map[config.log_level]))
+
+ ---@type overseer.Report
+ local info
+ commands.info(function(info_cb)
+ info = info_cb
+ end)
+
+ vim.wait(10000, function()
+ return info ~= nil
+ end)
+
+ if not info then
+ vim.health.warn("timeout waiting for tasks to generate")
+ return
+ end
+
+ local provider_names = vim.tbl_keys(info.providers)
+ table.sort(provider_names, function(a_name, b_name)
+ local a, b = info.providers[a_name], info.providers[b_name]
+ local a_err, b_err = a.message ~= nil, b.message ~= nil
+ if a_err ~= b_err then
+ return not a_err
+ end
+ return a_name < b_name
+ end)
+ for _, name in ipairs(provider_names) do
+ local provider_report = info.providers[name]
+ if name:match("^[%w_%.%-]+$") then
+ name = "{" .. name .. "}"
+ end
+ if provider_report.message then
+ vim.health.warn(string.format("%s: %s", name, provider_report.message))
+ else
+ if provider_report.from_cache then
+ name = name .. " (cached)"
+ elseif provider_report.elapsed_ms > 0 then
+ name = string.format("%s (%sms)", name, provider_report.elapsed_ms)
+ end
+ vim.health.ok(
+ string.format(
+ "%s: `%d/%d tasks available`",
+ name,
+ provider_report.available_tasks,
+ provider_report.total_tasks
+ )
+ )
+ end
+ end
+end
+
+return M
diff --git a/lua/overseer/init.lua b/lua/overseer/init.lua
index 29fc0f6d..8827da38 100644
--- a/lua/overseer/init.lua
+++ b/lua/overseer/init.lua
@@ -1,131 +1,6 @@
----@mod overseer
-
----@diagnostic disable: undefined-doc-param
-
local M = {}
-local setup_callbacks = {}
-
-local BREAKING_CHANGES_NOTICE =
- [[ATTN: overseer.nvim will experience breaking changes soon. Pin to version v1.6.0 or earlier to avoid disruption.
-See: https://github.com/stevearc/overseer.nvim/pull/448]]
-local initialized = false
-local pending_opts
-local function do_setup()
- if not pending_opts then
- if initialized then
- return
- else
- -- If user hasn't called setup(), assume an empty options table
- pending_opts = {}
- end
- end
- vim.notify_once(BREAKING_CHANGES_NOTICE, vim.log.levels.WARN)
- local config = require("overseer.config")
- local log = require("overseer.log")
- config.setup(pending_opts)
- pending_opts = nil
- local util = require("overseer.util")
- local success_color = util.find_success_color()
- for _, hl in ipairs(M.get_all_highlights()) do
- vim.cmd(string.format("hi default link %s %s", hl.name, hl.default))
- end
- local aug = vim.api.nvim_create_augroup("Overseer", {})
- if config.auto_detect_success_color then
- vim.api.nvim_create_autocmd("ColorScheme", {
- pattern = "*",
- group = aug,
- desc = "Set Overseer default success color",
- callback = function()
- success_color = util.find_success_color()
- vim.cmd(string.format("hi link OverseerSUCCESS %s", success_color))
- end,
- })
- end
- vim.api.nvim_create_autocmd("User", {
- pattern = "SessionSavePre",
- desc = "Save task state when vim-session saves",
- group = aug,
- callback = function()
- local task_list = require("overseer.task_list")
- local cmds = vim.g.session_save_commands
- local tasks = vim.tbl_map(function(task)
- return task:serialize()
- end, task_list.list_tasks({ bundleable = true }))
- -- Abort if no tasks or if not using vim-session (no vim.g.session_save_commands)
- if not cmds or vim.tbl_isempty(tasks) then
- return
- end
- table.insert(cmds, '" overseer.nvim')
- ---@type string
- local data = vim.json.encode(tasks) ---@diagnostic disable-line: assign-type-mismatch
- -- For some reason, vim.json.encode encodes / as \/.
- data = string.gsub(data, "\\/", "/")
- data = string.gsub(data, "'", "\\'")
- table.insert(cmds, string.format("lua require('overseer')._start_tasks('%s')", data))
- vim.g.session_save_commands = cmds
- end,
- })
- local Notifier = require("overseer.notifier")
- vim.api.nvim_create_autocmd("FocusGained", {
- desc = "Track editor focus for overseer",
- group = aug,
- callback = function()
- Notifier.focused = true
- end,
- })
- vim.api.nvim_create_autocmd("FocusLost", {
- desc = "Track editor focus for overseer",
- group = aug,
- callback = function()
- Notifier.focused = false
- end,
- })
- initialized = true
- for _, cb in ipairs(setup_callbacks) do
- local cb_ok, err = pcall(cb)
- if not cb_ok then
- log:error("Error running overseer setup callback: %s", err)
- end
- end
-end
-
----When this function is called, complete the overseer setup
----@param mod string Name of overseer module
----@param fn string Name of function to wrap
-local function lazy(mod, fn)
- return function(...)
- do_setup()
- return require(string.format("overseer.%s", mod))[fn](...)
- end
-end
-
----When this function is called, if overseer has not loaded yet defer the call until after overseer
----has loaded.
----@param mod string Name of overseer module
----@param fn string Name of function to wrap
-local function lazy_pend(mod, fn)
- return function(...)
- if initialized then
- require(string.format("overseer.%s", mod))[fn](...)
- else
- local args = { ... }
- local traceback = debug.traceback()
- M.on_setup(function()
- local log = require("overseer.log")
- local ok, module = pcall(require, string.format("overseer.%s", mod))
- if not ok then
- log:error("Error requiring overseer lazy module: %s", module)
- return
- end
- local call_ok, err = pcall(module[fn], unpack(args))
- if not call_ok then
- log:error("Error in overseer lazy call to %s.%s: %s\n%s", mod, fn, err, traceback)
- end
- end)
- end
- end
-end
+---@alias overseer.Serialized string|{[1]: string, [string]: any}
local commands = {
{
@@ -165,43 +40,6 @@ local commands = {
end,
},
},
- {
- cmd = "OverseerSaveBundle",
- args = "`[name]`",
- func = "_save_bundle",
- def = {
- desc = "Serialize and save the current tasks to disk",
- nargs = "?",
- },
- },
- {
- cmd = "OverseerLoadBundle",
- args = "`[name]`",
- func = "_load_bundle",
- def = {
- desc = "Load tasks that were saved to disk. With `!` tasks will not be started",
- nargs = "?",
- bang = true,
- },
- },
- {
- cmd = "OverseerDeleteBundle",
- args = "`[name]`",
- func = "_delete_bundle",
- def = {
- desc = "Delete a saved task bundle",
- nargs = "?",
- },
- },
- {
- cmd = "OverseerRunCmd",
- args = "`[command]`",
- func = "_run_command",
- def = {
- desc = "Run a raw shell command",
- nargs = "?",
- },
- },
{
cmd = "OverseerRun",
args = "`[name/tags]`",
@@ -212,26 +50,14 @@ local commands = {
},
},
{
- cmd = "OverseerInfo",
- func = "_info",
- def = {
- desc = "Display diagnostic information about overseer",
- },
- },
- {
- cmd = "OverseerBuild",
- func = "_build_task",
- def = {
- desc = "Open the task builder",
- },
- },
- {
- cmd = "OverseerQuickAction",
- args = "`[action]`",
- func = "_quick_action",
+ cmd = "OverseerShell",
+ args = "`[command]`",
+ func = "_run_shell",
def = {
- nargs = "?",
- desc = "Run an action on the most recent task, or the task under the cursor",
+ desc = "Run a shell command as an overseer task. With `!` the task is created but not started",
+ complete = "shellcmdline",
+ bang = true,
+ nargs = "*",
},
},
{
@@ -241,28 +67,20 @@ local commands = {
desc = "Select a task to run an action on",
},
},
- {
- cmd = "OverseerClearCache",
- func = "_clear_cache",
- def = {
- desc = "Clear the task cache",
- },
- },
}
local function create_commands()
for _, v in pairs(commands) do
- vim.api.nvim_create_user_command(v.cmd, lazy("commands", v.func), v.def)
+ vim.api.nvim_create_user_command(v.cmd, function(args)
+ require("overseer.commands")[v.func](args)
+ end, v.def)
end
end
----Add support for preLaunchTask/postDebugTask to nvim-dap
----@private
----@deprecated
----@param enabled boolean
-M.patch_dap = function(enabled)
- M.enable_dap(enabled)
-end
+M.builtin = {
+ jobstart = vim.fn.jobstart,
+ system = vim.system,
+}
---Add support for preLaunchTask/postDebugTask to nvim-dap
---This is enabled by default when you call overseer.setup() unless you set `dap = false`
@@ -280,7 +98,7 @@ M.enable_dap = function(enabled)
end
if not dap.listeners.on_config then
local log = require("overseer.log")
- log:warn("overseer requires a newer version of nvim-dap to enable DAP integration")
+ log.warn("overseer requires a newer version of nvim-dap to enable DAP integration")
return
end
if enabled then
@@ -298,32 +116,52 @@ M.enable_dap = function(enabled)
end
---Initialize overseer
----@param opts overseer.Config|nil Configuration options
+---@param opts overseer.SetupOpts|nil Configuration options
M.setup = function(opts)
- if vim.fn.has("nvim-0.8") == 0 then
- vim.notify_once(
- "overseer has dropped support for Neovim <0.8. Please use the nvim-0.7 branch or upgrade Neovim",
- vim.log.levels.ERROR
- )
+ opts = opts or {}
+ if not M.private_setup() then
return
end
- opts = opts or {}
- create_commands()
- M.enable_dap(opts.dap)
- pending_opts = opts
- if initialized then
- do_setup()
+ local config = require("overseer.config")
+ config.setup(opts)
+ M.enable_dap(config.dap)
+ M.wrap_builtins(config.experimental_wrap_builtins.enabled)
+end
+
+local function create_highlights()
+ for _, hl in ipairs(M.get_all_highlights()) do
+ vim.api.nvim_set_hl(0, hl.name, { link = hl.default, default = true })
end
end
----Add a callback to run after overseer lazy setup
----@param callback fun()
-M.on_setup = function(callback)
- if initialized then
- callback()
- else
- table.insert(setup_callbacks, callback)
+local did_setup = false
+---@private
+---@return boolean
+M.private_setup = function()
+ if vim.fn.has("nvim-0.11") == 0 then
+ vim.notify_once(
+ "overseer has dropped support for Neovim <0.11. Please use a different branch or upgrade Neovim",
+ vim.log.levels.ERROR
+ )
+ return false
end
+
+ if did_setup then
+ return true
+ end
+ did_setup = true
+
+ create_commands()
+ M.wrap_builtins()
+ create_highlights()
+ local aug = vim.api.nvim_create_augroup("Overseer", {})
+ vim.api.nvim_create_autocmd("ColorScheme", {
+ pattern = "*",
+ group = aug,
+ desc = "Update Overseer highlights",
+ callback = create_highlights,
+ })
+ return true
end
---Create a new Task
@@ -331,89 +169,111 @@ end
---@return overseer.Task
---@example
--- local task = overseer.new_task({
---- cmd = { "./build.sh" },
---- args = { "all" },
+--- cmd = { "./build.sh", "all" },
--- components = { { "on_output_quickfix", open = true }, "default" }
--- })
--- task:start()
-M.new_task = lazy("task", "new")
+M.new_task = function(opts)
+ ---@diagnostic disable-next-line: invisible
+ local data = opts.from_template
+ if data then
+ local template = require("overseer.template")
+ local tmpl
+ local done = false
+ template.get_by_name(data.name, data.search, function(t)
+ tmpl = t
+ done = true
+ end)
+ vim.wait(2000, function()
+ return done
+ end)
+ if not tmpl then
+ error(string.format("Could not find template '%s'", data.name))
+ end
+ local task
+ done = false
+ local build_opts = {
+ params = data.params,
+ env = data.env,
+ cwd = opts.cwd,
+ search = data.search,
+ disallow_prompt = true,
+ }
+ template.build_task(tmpl, build_opts, function(_, t)
+ done = true
+ task = t
+ end)
+ vim.wait(500, function()
+ return done
+ end)
+ if not task then
+ error(string.format("Error building task from template '%s'", data.name))
+ end
+ return task
+ else
+ return require("overseer.task").new(opts)
+ end
+end
---Open or close the task list
---@param opts nil|overseer.WindowOpts
-M.toggle = lazy("window", "toggle")
+M.toggle = function(opts)
+ return require("overseer.window").toggle(opts)
+end
---Open the task list
---@param opts nil|overseer.WindowOpts
---- enter boolean|nil If false, stay in current window. Default true
---- direction nil|"left"|"right" Which direction to open the task list
-M.open = lazy("window", "open")
+M.open = function(opts)
+ return require("overseer.window").open(opts)
+end
---Close the task list
-M.close = lazy("window", "close")
-
----Get the list of saved task bundles
----@return string[] Names of task bundles
-M.list_task_bundles = lazy("task_bundle", "list_task_bundles")
----Load tasks from a saved bundle
----@param name nil|string
----@param opts nil|table
---- ignore_missing nil|boolean When true, don't notify if bundle doesn't exist
---- autostart nil|boolean When true, start the tasks after loading (default true)
-M.load_task_bundle = lazy("task_bundle", "load_task_bundle")
----Save tasks to a bundle on disk
----@param name string|nil Name of bundle. If nil, will prompt user.
----@param tasks nil|overseer.Task[] Specific tasks to save. If nil, uses config.bundles.save_task_opts
----@param opts table|nil
---- on_conflict nil|"overwrite"|"append"|"cancel"
-M.save_task_bundle = lazy("task_bundle", "save_task_bundle")
----Delete a saved task bundle
----@param name string|nil
-M.delete_task_bundle = lazy("task_bundle", "delete_task_bundle")
+M.close = function()
+ return require("overseer.window").close()
+end
---List all tasks
---@param opts nil|overseer.ListTaskOpts
---@return overseer.Task[]
-M.list_tasks = lazy("task_list", "list_tasks")
+M.list_tasks = function(opts)
+ return require("overseer.task_list").list_tasks(opts)
+end
---Run a task from a template
---@param opts overseer.TemplateRunOpts
---@param callback nil|fun(task: overseer.Task|nil, err: string|nil)
----@note
---- The prompt option will control when the user is presented a popup dialog to input template
---- parameters. The possible values are:
---- always Show when template has any params
---- missing Show when template has any params not explicitly passed in
---- allow Only show when a required param is missing
---- avoid Only show when a required param with no default value is missing
---- never Never show prompt (error if required param missing)
---- The default is controlled by the default_template_prompt config option.
---@example
--- -- Run the task named "make all"
--- -- equivalent to :OverseerRun make\ all
---- overseer.run_template({name = "make all"})
+--- overseer.run_task({name = "make all"})
--- -- Run the default "build" task
--- -- equivalent to :OverseerRun BUILD
---- overseer.run_template({tags = {overseer.TAG.BUILD}})
+--- overseer.run_task({tags = {overseer.TAG.BUILD}})
--- -- Run the task named "serve" with some default parameters
---- overseer.run_template({name = "serve", params = {port = 8080}})
+--- overseer.run_task({name = "serve", params = {port = 8080}})
--- -- Create a task but do not start it
---- overseer.run_template({name = "make", autostart = false}, function(task)
+--- overseer.run_task({name = "make", autostart = false}, function(task)
--- -- do something with the task
--- end)
--- -- Run a task and immediately open the floating window
---- overseer.run_template({name = "make"}, function(task)
+--- overseer.run_task({name = "make"}, function(task)
--- if task then
--- overseer.run_action(task, 'open float')
--- end
--- end)
---- -- Run a task and always show the parameter prompt
---- overseer.run_template({name = "npm watch", prompt = "always"})
-M.run_template = lazy("commands", "run_template")
+M.run_task = function(opts, callback)
+ return require("overseer.commands").run_template(opts, callback)
+end
+
+---Use overseer.run_task
+---@deprecated
+M.run_template = function(opts, callback)
+ vim.deprecate("overseer.run_template", "overseer.run_task", "2026-01-01", "overseer.nvim")
+ return M.run_task(opts, callback)
+end
----Preload templates for run_template
----@param opts nil|table
---- dir string
---- ft nil|string
----@param cb nil|fun() Called when preloading is complete
+---Preload templates for run_task
+---@param opts? overseer.SearchParams
+---@param cb? fun() Called when preloading is complete
---@note
--- Typically this would be done to prevent a long wait time for :OverseerRun when using a slow
--- template provider.
@@ -423,78 +283,24 @@ M.run_template = lazy("commands", "run_template")
--- local cwd = vim.v.cwd or vim.fn.getcwd()
--- require("overseer").preload_task_cache({ dir = cwd })
--- })
-M.preload_task_cache = lazy("commands", "preload_cache")
----Clear cached templates for run_template
----@param opts nil|table
---- dir string
---- ft nil|string
-M.clear_task_cache = lazy("commands", "clear_cache")
+M.preload_task_cache = function(opts, cb)
+ return require("overseer.commands").preload_cache(opts, cb)
+end
+---Clear cached templates for run_task
+---@param opts? overseer.SearchParams
+M.clear_task_cache = function(opts)
+ return require("overseer.commands").clear_cache(opts)
+end
---Run an action on a task
---@param task overseer.Task
----@param name string|nil Name of action. When omitted, prompt user to pick.
-M.run_action = lazy("action_util", "run_task_action")
-
----Create a new template by overriding fields on another
----@param base overseer.TemplateFileDefinition The base template definition to wrap
----@param override nil|table Override any fields on the base
----@param default_params nil|table Provide default values for any parameters on the base
----@return overseer.TemplateFileDefinition
----@note
---- This is typically used for a TemplateProvider, to define the task a single time and generate
---- multiple templates based on the available args.
----@example
---- local tmpl = {
---- params = {
---- args = { type = 'list', delimiter = ' ' }
---- },
---- builder = function(params)
---- return {
---- cmd = { 'make' },
---- args = params.args,
---- }
---- }
---- local template_provider = {
---- name = "Some provider",
---- generator = function(opts, cb)
---- cb({
---- overseer.wrap_template(tmpl, nil, { args = { 'all' } }),
---- overseer.wrap_template(tmpl, {name = 'make clean'}, { args = { 'clean' } }),
---- })
---- end
---- }
-M.wrap_template = function(base, override, default_params)
- override = override or {}
- if default_params then
- local base_params = base.params
- if type(base_params) == "function" then
- override.params = function()
- local params = base_params()
- for k, v in pairs(default_params) do
- params[k].default = v
- params[k].optional = true
- end
- return params
- end
- else
- override.params = vim.deepcopy(base_params or {})
- for k, v in pairs(default_params) do
- override.params[k].default = v
- override.params[k].optional = true
- end
- end
- end
- setmetatable(override, { __index = base })
- ---@cast override overseer.TemplateFileDefinition
- return override
+---@param name? string Name of action. When omitted, prompt user to pick.
+M.run_action = function(task, name)
+ return require("overseer.action_util").run_task_action(task, name)
end
---Add a hook that runs on a TaskDefinition before the task is created
---@param opts nil|overseer.HookOptions When nil, run the hook on all templates
---- name nil|string Only run if the template name matches this pattern (using string.match)
---- module nil|string Only run if the template module matches this pattern (using string.match)
---- filetype nil|string|string[] Only run if the current file is one of these filetypes
---- dir nil|string|string[] Only run if inside one of these directories
---@param hook fun(task_defn: overseer.TaskDefinition, util: overseer.TaskUtil)
---@example
--- -- Add on_output_quickfix component to all "cargo" templates
@@ -511,7 +317,9 @@ end
--- GO111MODULE = "on"
--- })
--- end)
-M.add_template_hook = lazy_pend("template", "add_hook_template")
+M.add_template_hook = function(opts, hook)
+ require("overseer.template").add_hook_template(opts, hook)
+end
---Remove a hook that was added with add_template_hook
---@param opts nil|overseer.HookOptions Same as for add_template_hook
---@param hook fun(task_defn: overseer.TaskDefinition, util: overseer.TaskUtil)
@@ -523,7 +331,9 @@ M.add_template_hook = lazy_pend("template", "add_hook_template")
--- overseer.add_template_hook(opts, hook)
--- -- Remove should pass in the same opts as add
--- overseer.remove_template_hook(opts, hook)
-M.remove_template_hook = lazy_pend("template", "remove_hook_template")
+M.remove_template_hook = function(opts, hook)
+ require("overseer.template").remove_hook_template(opts, hook)
+end
---Directly register an overseer template
---@param defn overseer.TemplateDefinition|overseer.TemplateProvider
@@ -536,67 +346,122 @@ M.remove_template_hook = lazy_pend("template", "remove_hook_template")
--- }
--- end,
--- })
-M.register_template = lazy_pend("template", "register")
----Load a template definition from its module location
----@param name string
----@example
---- -- This will load the template in lua/overseer/template/mytask.lua
---- overseer.load_template('mytask')
-M.load_template = lazy_pend("template", "load_template")
-
----Open a tab with windows laid out for debugging a parser
-M.debug_parser = lazy("parser.debug", "start_debug_session")
+M.register_template = function(defn)
+ require("overseer.template").register(defn)
+end
---Register a new component alias.
---@param name string
---@param components overseer.Serialized[]
+---@param override? boolean When true, override any existing alias with the same name
---@note
--- This is intended to be used by plugin authors that wish to build on top of overseer. They do not
--- have control over the call to overseer.setup(), so this provides an alternative method of
--- setting a component alias that they can then use when creating tasks.
---@example
--- require("overseer").register_alias("my_plugin", { "default", "on_output_quickfix" })
-M.register_alias = lazy("component", "alias")
+M.register_alias = function(name, components, override)
+ return require("overseer.component").alias(name, components, override)
+end
+
+---Set a window to display the output of a dynamically-chosen task
+---@param winid? integer The window to use for displaying the task output
+---@param opts? overseer.TaskViewOpts
+---@example
+--- -- Always show the output from the most recent Neotest task in this window.
+--- -- Close it automatically when all test tasks are disposed.
+--- overseer.create_task_output_view(0, {
+--- select = function(self, tasks, task_under_cursor)
+--- for _, task in ipairs(tasks) do
+--- if task.metadata.neotest_group_id then
+--- return task
+--- end
+--- end
+--- self:dispose()
+--- end,
+--- })
+M.create_task_output_view = function(winid, opts)
+ require("overseer.task_view").new(winid, opts)
+end
--- Used for vim-session integration.
-local timer_active = false
+---@param cmd string|string[]
+---@param opts? table
+---@return any
+local wrapped_jobstart = function(cmd, opts)
+ local config = require("overseer.config")
+ local util = require("overseer.util")
+ local caller = util.get_caller()
+ -- TODO wrapping jobstart in a fast event is difficult because we call a lot of unsafe APIs
+ if vim.in_fast_event() or not config.experimental_wrap_builtins.condition(cmd, caller, opts) then
+ return M.builtin.jobstart(cmd, opts)
+ end
+ opts = opts or {}
+ local task = M.new_task({
+ cmd = cmd,
+ cwd = opts.cwd,
+ env = opts.env,
+ source = caller,
+ ephemeral = true,
+ strategy = { "jobstart", wrap_opts = opts },
+ components = { "default_builtin" },
+ })
+ task:start()
+ ---@diagnostic disable-next-line: invisible
+ local strat = task.strategy
+ ---@cast strat overseer.JobstartStrategy
+ return strat.job_id
+end
+---@param cmd string[]
+---@param opts? vim.SystemOpts
+---@param on_exit? fun(out: vim.SystemCompleted)
+---@return vim.SystemObj
+local wrapped_system = function(cmd, opts, on_exit)
+ local config = require("overseer.config")
+ local util = require("overseer.util")
+ local caller = util.get_caller()
+ -- TODO wrapping vim.system in a fast event is difficult because we call a lot of unsafe APIs
+ if vim.in_fast_event() or not config.experimental_wrap_builtins.condition(cmd, caller, opts) then
+ return M.builtin.system(cmd, opts, on_exit)
+ end
+ opts = opts or {}
+ local task = M.new_task({
+ cmd = cmd,
+ cwd = opts.cwd,
+ ---@diagnostic disable-next-line: assign-type-mismatch
+ env = opts.env,
+ source = caller,
+ ephemeral = true,
+ strategy = { "system", wrap_opts = opts, wrap_exit = on_exit },
+ components = { "default_builtin" },
+ })
+ task:start()
+ ---@diagnostic disable-next-line: invisible
+ local strat = task.strategy
+ ---@cast strat overseer.SystemStrategy
+ return strat.handle
+end
+
+local patched = false
+---Hook vim.system and vim.fn.jobstart to display tasks in overseer
---@private
-M._start_tasks = function(str)
- -- HACK for some reason vim-session fires SessionSavePre multiple times, which
- -- can lead to multiple 'load' lines in the same session file. We need to make
- -- sure we only take the first one.
- if timer_active then
+---@param enabled? boolean
+M.wrap_builtins = function(enabled)
+ if enabled == nil then
+ enabled = true
+ end
+ if patched == enabled then
return
end
- timer_active = true
- vim.defer_fn(function()
- ---@type any
- local data = vim.json.decode(str)
- for _, params in ipairs(data) do
- local task = M.new_task(params)
- task:start()
- end
- timer_active = false
- end, 100)
-end
+ patched = enabled
-setmetatable(M, {
- __index = function(t, key)
- local ok, val = pcall(require, string.format("overseer.%s", key))
- if ok then
- rawset(t, key, val)
- return val
- else
- -- allow top-level direct access to constants (e.g. overseer.STATUS)
- local constants = require("overseer.constants")
- if constants[key] then
- rawset(t, key, constants[key])
- return constants[key]
- end
- error(string.format("Error requiring overseer.%s: %s", key, val))
- end
- end,
-})
+ if patched then
+ vim.fn.jobstart = wrapped_jobstart
+ vim.system = wrapped_system
+ else
+ vim.fn.jobstart = M.builtin.jobstart
+ vim.system = M.builtin.system
+ end
+end
---Used for documentation generation
---@private
@@ -616,12 +481,10 @@ end
---Used for documentation generation
---@private
M.get_all_highlights = function()
- local util = require("overseer.util")
- local success_color = util.find_success_color()
return {
{ name = "OverseerPENDING", default = "Normal", desc = "Pending tasks" },
{ name = "OverseerRUNNING", default = "Constant", desc = "Running tasks" },
- { name = "OverseerSUCCESS", default = success_color, desc = "Succeeded tasks" },
+ { name = "OverseerSUCCESS", default = "DiagnosticOk", desc = "Succeeded tasks" },
{ name = "OverseerCANCELED", default = "DiagnosticWarn", desc = "Canceled tasks" },
{ name = "OverseerFAILURE", default = "DiagnosticError", desc = "Failed tasks" },
{ name = "OverseerDISPOSED", default = "Comment" },
@@ -649,4 +512,16 @@ M.get_all_highlights = function()
}
end
+setmetatable(M, {
+ __index = function(t, key)
+ -- allow top-level direct access to constants (e.g. overseer.STATUS)
+ local constants = require("overseer.constants")
+ if constants[key] then
+ rawset(t, key, constants[key])
+ return constants[key]
+ end
+ return rawget(t, key)
+ end,
+})
+
return M
diff --git a/lua/overseer/keymap_util.lua b/lua/overseer/keymap_util.lua
new file mode 100644
index 00000000..76b27a52
--- /dev/null
+++ b/lua/overseer/keymap_util.lua
@@ -0,0 +1,166 @@
+local keymaps = require("overseer.task_list.keymaps")
+local layout = require("overseer.layout")
+local util = require("overseer.util")
+
+local M = {}
+
+---@param rhs string|table|fun()
+---@return string|fun() rhs
+---@return table opts
+---@return string|nil mode
+local function resolve(rhs)
+ if type(rhs) == "string" and vim.startswith(rhs, "keymap.") then
+ local action_name = vim.split(rhs, ".", { plain = true })[2]
+ local action = keymaps[action_name]
+ if not action then
+ vim.notify("[overseer.nvim] Unknown action name: " .. action_name, vim.log.levels.ERROR)
+ end
+ return resolve(action)
+ elseif type(rhs) == "table" then
+ local opts = vim.deepcopy(rhs)
+ -- We support passing in a `callback` key, or using the 1 index as the rhs of the keymap
+ local callback, parent_opts = resolve(opts.callback or opts[1])
+
+ -- Fall back to the parent desc, adding the opts as a string if it exists
+ if parent_opts.desc and not opts.desc then
+ if opts.opts then
+ opts.desc =
+ string.format("%s %s", parent_opts.desc, vim.inspect(opts.opts):gsub("%s+", " "))
+ else
+ opts.desc = parent_opts.desc
+ end
+ end
+
+ local mode = opts.mode
+ if type(rhs.callback) == "string" then
+ local action_opts, action_mode
+ callback, action_opts, action_mode = resolve(rhs.callback)
+ opts = vim.tbl_extend("keep", opts, action_opts)
+ mode = mode or action_mode
+ end
+
+ -- remove all the keys that we can't pass as options to `vim.keymap.set`
+ opts.callback = nil
+ opts.mode = nil
+ opts[1] = nil
+ opts.deprecated = nil
+ opts.parameters = nil
+
+ if opts.opts and type(callback) == "function" then
+ local callback_args = opts.opts
+ opts.opts = nil
+ local orig_callback = callback
+ callback = function()
+ ---@diagnostic disable-next-line: redundant-parameter
+ orig_callback(callback_args)
+ end
+ end
+
+ return callback, opts, mode
+ else
+ return rhs, {}
+ end
+end
+
+---@param new_keymaps table
+---@param bufnr integer
+M.set_keymaps = function(new_keymaps, bufnr)
+ for k, v in pairs(new_keymaps) do
+ local rhs, opts, mode = resolve(v)
+ if rhs then
+ vim.keymap.set(mode or "", k, rhs, vim.tbl_extend("keep", { buffer = bufnr }, opts))
+ end
+ end
+end
+
+---@param maps table
+M.show_help = function(maps)
+ local rhs_to_lhs = {}
+ local lhs_to_all_lhs = {}
+ for k, rhs in pairs(maps) do
+ if rhs then
+ if rhs_to_lhs[rhs] then
+ local first_lhs = rhs_to_lhs[rhs]
+ table.insert(lhs_to_all_lhs[first_lhs], k)
+ else
+ rhs_to_lhs[rhs] = k
+ lhs_to_all_lhs[k] = { k }
+ end
+ end
+ end
+
+ local max_lhs = 1
+ local keymap_entries = {}
+ for k, rhs in pairs(maps) do
+ local all_lhs = lhs_to_all_lhs[k]
+ if all_lhs then
+ local _, opts = resolve(rhs)
+ local keystr = table.concat(all_lhs, "/")
+ max_lhs = math.max(max_lhs, vim.api.nvim_strwidth(keystr))
+ table.insert(keymap_entries, { str = keystr, all_lhs = all_lhs, desc = opts.desc or "" })
+ end
+ end
+ table.sort(keymap_entries, function(a, b)
+ return a.desc < b.desc
+ end)
+
+ local lines = {}
+ local highlights = {}
+ local max_line = 1
+ for _, entry in ipairs(keymap_entries) do
+ local line = string.format(" %s %s", util.align(entry.str, max_lhs, "left"), entry.desc)
+ max_line = math.max(max_line, vim.api.nvim_strwidth(line))
+ table.insert(lines, line)
+ local start = 1
+ for _, key in ipairs(entry.all_lhs) do
+ local keywidth = vim.api.nvim_strwidth(key)
+ table.insert(highlights, { "Special", #lines, start, start + keywidth })
+ start = start + keywidth + 1
+ end
+ end
+
+ local bufnr = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
+ local ns = vim.api.nvim_create_namespace("Overseer")
+ for _, hl in ipairs(highlights) do
+ local hl_group, lnum, start_col, end_col = unpack(hl)
+ vim.api.nvim_buf_set_extmark(bufnr, ns, lnum - 1, start_col, {
+ end_col = end_col,
+ hl_group = hl_group,
+ })
+ end
+ vim.keymap.set("n", "q", "close", { buffer = bufnr })
+ vim.keymap.set("n", "", "close", { buffer = bufnr })
+ vim.bo[bufnr].modifiable = false
+ vim.bo[bufnr].bufhidden = "wipe"
+
+ local editor_width = vim.o.columns
+ local editor_height = layout.get_editor_height()
+ local winid = vim.api.nvim_open_win(bufnr, true, {
+ relative = "editor",
+ row = math.max(0, (editor_height - #lines) / 2),
+ col = math.max(0, (editor_width - max_line - 1) / 2),
+ width = math.min(editor_width, max_line + 1),
+ height = math.min(editor_height, #lines),
+ zindex = 150,
+ style = "minimal",
+ })
+ local function close()
+ if vim.api.nvim_win_is_valid(winid) then
+ vim.api.nvim_win_close(winid, true)
+ end
+ end
+ vim.api.nvim_create_autocmd("BufLeave", {
+ callback = close,
+ once = true,
+ nested = true,
+ buffer = bufnr,
+ })
+ vim.api.nvim_create_autocmd("WinLeave", {
+ callback = close,
+ once = true,
+ nested = true,
+ })
+end
+
+return M
diff --git a/lua/overseer/layout.lua b/lua/overseer/layout.lua
index 8ac50a45..8d859e6d 100644
--- a/lua/overseer/layout.lua
+++ b/lua/overseer/layout.lua
@@ -1,11 +1,17 @@
local config = require("overseer.config")
local M = {}
+---@param value number
+---@return boolean
local function is_float(value)
local _, p = math.modf(value)
return p ~= 0
end
+---@generic T
+---@param value T
+---@param max_value integer
+---@return T
local function calc_float(value, max_value)
if value and is_float(value) then
return math.min(max_value, value * max_value)
@@ -14,10 +20,12 @@ local function calc_float(value, max_value)
end
end
+---@return integer
M.get_editor_width = function()
return vim.o.columns
end
+---@return integer
M.get_editor_height = function()
local editor_height = vim.o.lines - vim.o.cmdheight
-- Subtract 1 if tabline is visible
@@ -33,6 +41,11 @@ M.get_editor_height = function()
return editor_height
end
+---@param values? number|number[]
+---@param max_value? integer
+---@param aggregator fun(a: number, b: number): number
+---@param limit number
+---@return nil|integer
local function calc_list(values, max_value, aggregator, limit)
local ret = limit
if not max_value or not values then
@@ -48,6 +61,12 @@ local function calc_list(values, max_value, aggregator, limit)
return ret
end
+---@param desired_size? integer
+---@param exact_size? integer
+---@param min_size? number|number[]
+---@param max_size? number|number[]
+---@param total_size integer
+---@return integer
local function calculate_dim(desired_size, exact_size, min_size, max_size, total_size)
local ret = calc_float(exact_size, total_size)
local min_val = calc_list(min_size, total_size, math.max, 1)
@@ -72,6 +91,17 @@ local function calculate_dim(desired_size, exact_size, min_size, max_size, total
return math.floor(ret)
end
+---@class (exact) overseer.LayoutOpts
+---@field width? number
+---@field height? number
+---@field min_width? number|number[]
+---@field max_width? number|number[]
+---@field min_height? number|number[]
+---@field max_height? number|number[]
+
+---@param desired_width? integer
+---@param opts overseer.LayoutOpts
+---@return integer
M.calculate_width = function(desired_width, opts)
return calculate_dim(
desired_width,
@@ -82,6 +112,9 @@ M.calculate_width = function(desired_width, opts)
)
end
+---@param desired_height? integer
+---@param opts overseer.LayoutOpts
+---@return integer
M.calculate_height = function(desired_height, opts)
return calculate_dim(
desired_height,
@@ -106,7 +139,6 @@ M.open_fullscreen_float = function(bufnr)
col = col,
width = width,
height = height,
- border = conf.border,
zindex = conf.zindex,
style = "minimal",
})
diff --git a/lua/overseer/log.lua b/lua/overseer/log.lua
index 0a81901d..b70d5a34 100644
--- a/lua/overseer/log.lua
+++ b/lua/overseer/log.lua
@@ -1,34 +1,30 @@
-local files = require("overseer.files")
-local LogHandler = {}
+local config = require("overseer.config")
local levels_reverse = {}
for k, v in pairs(vim.log.levels) do
levels_reverse[v] = k
end
-function LogHandler.new(opts)
- vim.validate({
- type = { opts.type, "s" },
- handle = { opts.handle, "f" },
- formatter = { opts.formatter, "f" },
- level = { opts.level, "n", true },
- })
- return setmetatable({
- type = opts.type,
- handle = opts.handle,
- formatter = opts.formatter,
- level = opts.level or vim.log.levels.INFO,
- }, { __index = LogHandler })
-end
+local Log = {}
+
+---@type integer
+Log.level = vim.log.levels.WARN
-function LogHandler:log(level, msg, ...)
- if self.level <= level then
- local text = self.formatter(level, msg, ...)
- self.handle(level, text)
+---@return string
+Log.get_logfile = function()
+ local ok, stdpath = pcall(vim.fn.stdpath, "log")
+ if not ok then
+ stdpath = vim.fn.stdpath("cache")
end
+ assert(type(stdpath) == "string")
+ return vim.fs.joinpath(stdpath, "overseer.log")
end
-local function default_formatter(level, msg, ...)
+---@param level integer
+---@param msg string
+---@param ... any[]
+---@return string
+local function format(level, msg, ...)
local args = vim.F.pack_len(...)
for i = 1, args.n do
local v = args[i]
@@ -39,165 +35,97 @@ local function default_formatter(level, msg, ...)
end
end
local ok, text = pcall(string.format, msg, vim.F.unpack_len(args))
+ local timestr = vim.fn.strftime("%Y-%m-%d %H:%M:%S")
if ok then
local str_level = levels_reverse[level]
- return string.format("[%s] %s", str_level, text)
+ return string.format("%s[%s] %s", timestr, str_level, text)
else
- return string.format("[ERROR] error formatting log line: '%s' args %s", msg, vim.inspect(args))
+ return string.format(
+ "%s[ERROR] error formatting log line: '%s' args %s",
+ timestr,
+ vim.inspect(msg),
+ vim.inspect(args)
+ )
end
end
-local function create_file_handler(opts)
- vim.validate({
- filename = { opts.filename, "s" },
- })
- local ok, stdpath = pcall(vim.fn.stdpath, "log")
- if not ok then
- stdpath = vim.fn.stdpath("cache")
+---@param line string
+local function write(line)
+ -- This will be replaced during initialization
+end
+
+local initialized = false
+local function initialize()
+ if initialized then
+ return
+ end
+ initialized = true
+ local filepath = Log.get_logfile()
+
+ local stat = vim.uv.fs_stat(filepath)
+ if stat and stat.size > 10 * 1024 * 1024 then
+ local backup = filepath .. ".1"
+ vim.uv.fs_unlink(backup)
+ vim.uv.fs_rename(filepath, backup)
end
- local filepath = files.join(stdpath, opts.filename)
+
+ local parent = vim.fs.dirname(filepath)
+ vim.fn.mkdir(parent, "p")
+
local logfile, openerr = io.open(filepath, "a+")
if not logfile then
- local err_msg = string.format("Failed to open Overseer log file: %s", openerr)
+ local err_msg = string.format("Failed to open overseer.nvim log file: %s", openerr)
vim.notify(err_msg, vim.log.levels.ERROR)
- opts.handle = function() end
else
- opts.handle = function(level, text)
- logfile:write(text)
+ write = function(line)
+ logfile:write(line)
logfile:write("\n")
logfile:flush()
end
end
- return LogHandler.new(opts)
-end
-
-local function create_notify_handler(opts)
- opts.handle = function(level, text)
- vim.notify(text, level)
- end
- return LogHandler.new(opts)
end
-local function create_echo_handler(opts)
- opts.handle = function(level, text)
- local hl = "Normal"
- if level == vim.log.levels.ERROR then
- hl = "DiagnosticError"
- elseif level == vim.log.levels.WARN then
- hl = "DiagnosticWarn"
+---Override the file handler e.g. for tests
+---@param handler fun(line: string)
+function Log.set_handler(handler)
+ write = handler
+ initialized = true
+end
+
+function Log.log(level, msg, ...)
+ if config.log_level <= level then
+ initialize()
+ if vim.in_fast_event() then
+ local splat = vim.F.pack_len(...)
+ vim.schedule(function()
+ local text = format(level, msg, vim.F.unpack_len(splat))
+ write(text)
+ end)
+ else
+ local text = format(level, msg, ...)
+ write(text)
end
- vim.api.nvim_echo({ { text, hl } }, true, {})
- end
- return LogHandler.new(opts)
-end
-
-local function create_null_handler()
- return LogHandler.new({
- formatter = function() end,
- handle = function() end,
- })
-end
-
-local function create_handler(opts)
- vim.validate({
- type = { opts.type, "s" },
- })
- if not opts.formatter then
- opts.formatter = default_formatter
- end
- if opts.type == "file" then
- return create_file_handler(opts)
- elseif opts.type == "notify" then
- return create_notify_handler(opts)
- elseif opts.type == "echo" then
- return create_echo_handler(opts)
- else
- vim.notify(string.format("Unknown log handler %s", opts.type), vim.log.levels.ERROR)
- return create_null_handler()
- end
-end
-
-local Log = {}
-
-function Log.new(opts)
- vim.validate({
- handlers = { opts.handlers, "t" },
- level = { opts.level, "n", true },
- })
- local handlers = {}
- for _, defn in ipairs(opts.handlers) do
- table.insert(handlers, create_handler(defn))
- end
- local log = setmetatable({
- handlers = handlers,
- }, { __index = Log })
- if opts.level then
- log:set_level(opts.level)
- end
- return log
-end
-
-function Log:set_level(level)
- for _, handler in ipairs(self.handlers) do
- handler.level = level
end
end
-function Log:get_handlers()
- return self.handlers
+function Log.trace(...)
+ Log.log(vim.log.levels.TRACE, ...)
end
-function Log:log(level, msg, ...)
- for _, handler in ipairs(self.handlers) do
- handler:log(level, msg, ...)
- end
+function Log.debug(...)
+ Log.log(vim.log.levels.DEBUG, ...)
end
-function Log:trace(...)
- self:log(vim.log.levels.TRACE, ...)
+function Log.info(...)
+ Log.log(vim.log.levels.INFO, ...)
end
-function Log:debug(...)
- self:log(vim.log.levels.DEBUG, ...)
+function Log.warn(...)
+ Log.log(vim.log.levels.WARN, ...)
end
-function Log:info(...)
- self:log(vim.log.levels.INFO, ...)
+function Log.error(...)
+ Log.log(vim.log.levels.ERROR, ...)
end
-function Log:warn(...)
- self:log(vim.log.levels.WARN, ...)
-end
-
-function Log:error(...)
- self:log(vim.log.levels.ERROR, ...)
-end
-
-local root = Log.new({
- handlers = {
- {
- type = "echo",
- level = vim.log.levels.WARN,
- },
- },
-})
-
-local M = {}
-
-M.new = Log.new
-
-M.set_root = function(logger)
- root = logger
-end
-
-M.get_root = function()
- return root
-end
-
-setmetatable(M, {
- __index = function(_, key)
- return root[key]
- end,
-})
-
-return M
+return Log
diff --git a/lua/overseer/notifier.lua b/lua/overseer/notifier.lua
index 13e920e1..5ce98c13 100644
--- a/lua/overseer/notifier.lua
+++ b/lua/overseer/notifier.lua
@@ -1,12 +1,38 @@
local files = require("overseer.files")
local log = require("overseer.log")
+local overseer = require("overseer")
+
local Notifier = { focused = true }
---@class overseer.NotifierParams
---@field system "always"|"never"|"unfocused"
+local initialized = false
+local function create_autocmds()
+ if initialized then
+ return
+ end
+ initialized = true
+ local aug = vim.api.nvim_create_augroup("Overseer", { clear = false })
+ vim.api.nvim_create_autocmd("FocusGained", {
+ desc = "Track editor focus for overseer",
+ group = aug,
+ callback = function()
+ Notifier.focused = true
+ end,
+ })
+ vim.api.nvim_create_autocmd("FocusLost", {
+ desc = "Track editor focus for overseer",
+ group = aug,
+ callback = function()
+ Notifier.focused = false
+ end,
+ })
+end
+
---@param opts? overseer.NotifierParams
function Notifier.new(opts)
+ create_autocmds()
opts = vim.tbl_deep_extend("keep", opts or {}, {
system = "never",
})
@@ -15,37 +41,29 @@ function Notifier.new(opts)
end
local function system_notify(message, level)
- local job_id
if files.is_windows then
-- TODO
- log:warn("System notifications are not supported on Windows yet")
+ log.warn("System notifications are not supported on Windows yet")
return
elseif files.is_mac then
local cmd = {
"osascript",
"-e",
- string.format('display notification "%s" with title "%s"', "Overseer task complete", message),
+ string.format('display notification "Overseer task complete" with title "%s"', message),
}
if vim.fn.executable("reattach-to-user-namespace") == 1 then
table.insert(cmd, 1, "reattach-to-user-namespace")
end
- job_id = vim.fn.jobstart(cmd, {
- stdin = "null",
- })
+ overseer.builtin.system(cmd, {})
else
local urgency = level == vim.log.levels.INFO and "normal" or "critical"
- job_id = vim.fn.jobstart({
+ overseer.builtin.system({
"notify-send",
"-u",
urgency,
"Overseer task complete",
message,
- }, {
- stdin = "null",
- })
- end
- if job_id <= 0 then
- log:warn("Error performing system notification")
+ }, {})
end
end
diff --git a/lua/overseer/parselib.lua b/lua/overseer/parselib.lua
new file mode 100644
index 00000000..d9253fbd
--- /dev/null
+++ b/lua/overseer/parselib.lua
@@ -0,0 +1,311 @@
+local M = {}
+
+---@class (exact) overseer.OutputParser
+---@field parse fun(self: overseer.OutputParser, line: string) Called repeatedly with each line of output
+---@field get_result fun(self: overseer.OutputParser): table Mapping of result keys to parsed values. Usually contains a "diagnostics" key with a list of quickfix entries.
+---@field reset fun(self: overseer.OutputParser) Reset the parser to its initial state
+---@field result_version? number For background parsers only, this number should be bumped to indicate that the task should set the result while the process is still running
+
+---@alias overseer.TestFn fun(line: string): boolean
+---@alias overseer.ParseFn fun(line: string): nil|vim.quickfix.entry
+---@alias overseer.MatchFn fun(line: string): nil|string[]
+
+---@alias overseer.FieldProcessor fun(match: string, item: vim.quickfix.entry, field: string): any
+---@alias overseer.ParseFieldWithConversion {[1]: string, [2]: overseer.FieldProcessor}
+
+---@alias overseer.ParseField string|overseer.ParseFieldWithConversion
+
+---@type overseer.FieldProcessor
+local function default_postprocess_field(value, _, field)
+ if value:match("^%d+$") then
+ return tonumber(value)
+ elseif field == "type" then
+ return value:upper():match("^%w")
+ else
+ return value
+ end
+end
+
+---Create a match function from a lua pattern
+---@param pattern string lua pattern
+---@return overseer.MatchFn
+---@example
+--- local match_fn = parselib.make_lua_match_fn("^(%S+):(%d+):(%d+): (.+)$")
+--- local parse_fn = parselib.make_parse_fn(match_fn, {"filename", "lnum", "col", "text"})
+--- local parser = parselib.make_parser(parse_fn)
+M.make_lua_match_fn = function(pattern)
+ return function(line)
+ local ret = { line:match(pattern) }
+ if vim.tbl_isempty(ret) then
+ return nil
+ end
+ return ret
+ end
+end
+
+---Create a test function (returns true/false) from a lua pattern
+---@param pattern string lua pattern
+---@return overseer.TestFn
+---@example
+--- local test_fn = parselib.make_lua_test_fn("^File change detected")
+M.make_lua_test_fn = function(pattern)
+ return function(line)
+ local matched = line:match(pattern)
+ return matched ~= nil
+ end
+end
+
+---Create a match function from a vim regex
+---@param pattern string vim regex, passed to vim.fn.matchlist
+---@return overseer.MatchFn
+---@example
+--- local match_fn = parselib.make_regex_match_fn("\\v^(\\S+):(\\d+):(\\d+): (.+)$")
+--- local parse_fn = parselib.make_parse_fn(match_fn, {"filename", "lnum", "col", "text"})
+--- local parser = parselib.make_parser(parse_fn)
+M.make_regex_match_fn = function(pattern)
+ return function(line)
+ local result = vim.fn.matchlist(line, pattern)
+ if vim.tbl_isempty(result) then
+ return nil
+ end
+ table.remove(result, 1)
+ -- matchlist() will use "" if an optional submatch does not match, and it also throws a
+ -- bunch of "" on the end of the list just for funzies.
+ for i, v in ipairs(result) do
+ if v == "" then
+ result[i] = nil
+ end
+ end
+ return result
+ end
+end
+
+---Create a test function (returns true/false) from a match function
+---@param match overseer.MatchFn function that parses a line into a list of values
+---@return overseer.TestFn
+---@example
+--- local match_fn = parselib.make_lua_match_fn("^(%S+):(%d+):(%d+): (.+)$")
+--- local test_fn = parselib.match_to_test_fn(match_fn)
+M.match_to_test_fn = function(match)
+ return function(line)
+ local ret = match(line)
+ return ret ~= nil
+ end
+end
+
+---Create a function that parses a line into a quickfix entry
+---@param match overseer.MatchFn function that parses a line into a list of values
+---@param fields overseer.ParseField[] list of field names, or {field_name, postprocess_fn} tuples
+---@return overseer.ParseFn
+---@example
+--- local match_fn = parselib.make_lua_match_fn("^(%S+):(%d+):(%d+): (.+)$")
+--- local parse_fn = parselib.make_parse_fn(match_fn, {"filename", "lnum", "col", "text"})
+--- local parser = parselib.make_parser(parse_fn)
+M.make_parse_fn = function(match, fields)
+ return function(line)
+ local result = match(line)
+ if not result then
+ return nil
+ end
+ local item
+ for i, field in ipairs(fields) do
+ if result[i] then
+ if not item then
+ item = {}
+ end
+ local key, postprocess
+ if type(field) == "table" then
+ key, postprocess = field[1], field[2]
+ else
+ key = field
+ end
+ if not postprocess then
+ postprocess = default_postprocess_field
+ end
+ if key ~= "_" then
+ item[key] = postprocess(result[i], item, key)
+ end
+ end
+ end
+ return item
+ end
+end
+
+---Create a parser from a vim errorformat
+---@param errorformat string vim errorformat string
+---@return overseer.OutputParser
+---@example
+--- local parser = parselib.parser_from_errorformat("%f:%l: %m")
+M.parser_from_errorformat = function(errorformat)
+ local result = {}
+ local pending_lines = {}
+ local last_item_pending = false
+ ---@type overseer.OutputParser
+ return {
+ parse = function(_, line)
+ table.insert(pending_lines, line)
+ local items = vim.fn.getqflist({
+ lines = pending_lines,
+ efm = errorformat,
+ }).items
+ local valid_items = vim.tbl_filter(function(item)
+ return item.valid == 1
+ end, items)
+
+ if #valid_items > 1 then
+ table.insert(result, valid_items[2])
+ last_item_pending = true
+ pending_lines = { line }
+ elseif #valid_items == 1 then
+ if last_item_pending then
+ result[#result] = valid_items[1]
+ else
+ table.insert(result, valid_items[1])
+ end
+ last_item_pending = true
+ else
+ last_item_pending = false
+ pending_lines = {}
+ end
+ end,
+ get_result = function()
+ return { diagnostics = result }
+ end,
+ reset = function()
+ result = {}
+ end,
+ }
+end
+
+---Create a parser from a parse function
+---@param parse_fn overseer.ParseFn function that parses a line into a quickfix entry
+---@param results_key? string The key to put matches in the results table. defaults to "diagnostics"
+---@return overseer.OutputParser
+---@example
+--- local match_fn = parselib.make_lua_match_fn("^(%S+):(%d+):(%d+): (.+)$")
+--- local parse_fn = parselib.make_parse_fn(match_fn, {"filename", "lnum", "col", "text"})
+--- local parser = parselib.make_parser(parse_fn)
+M.make_parser = function(parse_fn, results_key)
+ if not results_key then
+ results_key = "diagnostics"
+ end
+ local result = {}
+ ---@type overseer.OutputParser
+ return {
+ parse = function(_, line)
+ local item = parse_fn(line)
+ if item then
+ table.insert(result, item)
+ end
+ end,
+ get_result = function()
+ return { [results_key] = result }
+ end,
+ reset = function()
+ result = {}
+ end,
+ }
+end
+
+---Combine multiple parsers into a single one (will merge the results)
+---@param parsers overseer.OutputParser[]
+---@return overseer.OutputParser
+---@example
+--- local match_fn = parselib.make_lua_match_fn("^(%S+):(%d+):(%d+): (.+)$")
+--- local parse_fn = parselib.make_parse_fn(match_fn, {"filename", "lnum", "col", "text"})
+--- local parser1 = parselib.make_parser(parse_fn)
+--- local parser2 = parselib.parser_from_errorformat("%f:%l: %m")
+--- local combined_parser = parselib.combine_parsers({parser1, parser2})
+M.combine_parsers = function(parsers)
+ ---@type overseer.OutputParser
+ return {
+ result_version = 0,
+ parse = function(self, line)
+ local version = 0
+ for _, parser in ipairs(parsers) do
+ parser:parse(line)
+ if parser.result_version then
+ version = version + parser.result_version
+ end
+ end
+ self.result_version = version
+ end,
+ get_result = function()
+ local ret = {}
+ for _, parser in ipairs(parsers) do
+ local res = parser:get_result()
+ for k, v in pairs(res) do
+ if not ret[k] then
+ ret[k] = v
+ elseif vim.islist(v) and vim.islist(ret[k]) then
+ local new_list = vim.list_extend({}, ret[k])
+ vim.list_extend(new_list, v)
+ ret[k] = new_list
+ elseif type(v) == "table" and type(ret[k]) == "table" then
+ ret[k] = vim.tbl_deep_extend("force", ret[k], v)
+ else
+ ret[k] = v
+ end
+ end
+ end
+ return ret
+ end,
+ reset = function()
+ for _, parser in ipairs(parsers) do
+ parser:reset()
+ end
+ end,
+ }
+end
+
+---@class overseer.BackgroundParserOpts
+---@field active_on_start? boolean Whether the parser should be active immediately or wait for the start_fn to begin parsing
+---@field start_fn? overseer.TestFn Function that tests whether to start parsing
+---@field end_fn? overseer.TestFn Function that tests whether to stop parsing
+
+---Wrap a parser and only activate it in between a matching start and end lines
+---@param parser overseer.OutputParser
+---@param opts overseer.BackgroundParserOpts
+---@return overseer.OutputParser
+---@example
+--- local base_parser = parselib.parser_from_errorformat("%f:%l: %m")
+--- local parser = parselib.wrap_background_parser(base_parser, {
+--- start_fn = parselib.make_lua_test_fn("^Starting analysis...$"),
+--- end_fn = parselib.make_lua_test_fn("^Analysis complete.$"),
+--- })
+M.wrap_background_parser = function(parser, opts)
+ local is_active = opts.active_on_start
+ ---@type overseer.OutputParser
+ return {
+ result_version = 0,
+ parse = function(self, line)
+ if is_active then
+ if opts.end_fn and opts.end_fn(line) then
+ self.result_version = self.result_version + 1
+ is_active = false
+ else
+ parser:parse(line)
+ end
+ elseif opts.start_fn and opts.start_fn(line) then
+ is_active = true
+ parser:reset()
+ -- Only bump the version if we have ever had results.
+ -- If we have previously set results on the task, hitting the start pattern again should
+ -- clear them immediately.
+ if self.result_version ~= 0 then
+ self.result_version = self.result_version + 1
+ end
+ end
+ end,
+ get_result = function()
+ return parser:get_result()
+ end,
+ reset = function(self)
+ self.result_version = 0
+ is_active = opts.active_on_start
+ parser:reset()
+ end,
+ }
+end
+
+return M
diff --git a/lua/overseer/parser/always.lua b/lua/overseer/parser/always.lua
deleted file mode 100644
index 54d2eb79..00000000
--- a/lua/overseer/parser/always.lua
+++ /dev/null
@@ -1,54 +0,0 @@
-local parser = require("overseer.parser")
-local util = require("overseer.parser.util")
-local Always = {
- desc = "A decorator that always returns SUCCESS",
- doc_args = {
- {
- name = "succeed",
- type = "boolean",
- desc = "Set to false to always return FAILURE",
- default = true,
- position_optional = true,
- },
- {
- name = "child",
- type = "parser",
- desc = "The child parser node",
- },
- },
- examples = {
- {
- desc = [[An extract node that returns SUCCESS even when it fails]],
- code = [[
- {"always",
- {"extract", "^([^%s].+):(%d+): (.+)$", "filename", "lnum", "text" }
- }
-]],
- },
- },
-}
-
-function Always.new(succeed, child)
- if child == nil then
- child = succeed
- succeed = true
- end
- return setmetatable({
- child = util.hydrate(child),
- succeed = succeed,
- }, { __index = Always })
-end
-
-function Always:reset()
- self.child:reset()
-end
-
-function Always:ingest(...)
- local st = self.child:ingest(...)
- if st == parser.STATUS.RUNNING then
- return st
- end
- return self.succeed and parser.STATUS.SUCCESS or parser.STATUS.FAILURE
-end
-
-return Always
diff --git a/lua/overseer/parser/append.lua b/lua/overseer/parser/append.lua
deleted file mode 100644
index 20a61ae9..00000000
--- a/lua/overseer/parser/append.lua
+++ /dev/null
@@ -1,39 +0,0 @@
-local parser = require("overseer.parser")
-local Append = {
- desc = "Append the current item to the results list",
- long_desc = "Normally the 'extract' node appends for you, but in cases where you use extract with `append = false`, you can explicitly append without extracting using this node.",
- doc_args = {
- {
- name = "opts",
- type = "object",
- desc = "Configuration options",
- position_optional = true,
- fields = {
- {
- name = "postprocess",
- type = "function",
- desc = "Call this function to do post-extraction processing on the values",
- },
- },
- },
- },
-}
-
-function Append.new(opts)
- opts = opts or {}
- return setmetatable({
- postprocess = opts.postprocess,
- }, { __index = Append })
-end
-
-function Append:reset() end
-
-function Append:ingest(line, ctx)
- if self.postprocess then
- self.postprocess(ctx.item, ctx)
- end
- parser.util.append_item(true, line, ctx)
- return parser.STATUS.SUCCESS
-end
-
-return Append
diff --git a/lua/overseer/parser/debug.lua b/lua/overseer/parser/debug.lua
deleted file mode 100644
index caa767a6..00000000
--- a/lua/overseer/parser/debug.lua
+++ /dev/null
@@ -1,196 +0,0 @@
----@mod overseer.parser.debug
----Provides an environment for writing and debugging parsers
-local files = require("overseer.files")
-local parser = require("overseer.parser")
-local util = require("overseer.util")
-local M = {}
-
-local source_buf
-local output_buf
-local input_buf
-
-local function get_filepath(filename)
- return files.join(vim.fn.stdpath("cache"), "overseer", filename)
-end
-
-local function load_parser()
- local bufnr = source_buf
- if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then
- return
- end
- local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
- local text = table.concat(lines, "\n")
- parser.trace(true)
- local builder = assert(loadstring(text))
- local ok, ret = pcall(builder)
- if ok then
- if ret.ingest then
- return ret
- else
- return nil,
- string.format("Expected parser to have method 'ingest'. Found %s", vim.inspect(ret))
- end
- else
- return nil, ret
- end
-end
-
-local parser_status_to_hl = setmetatable({
- RESET = "OverseerCanceled",
-}, {
- __index = function(_, key)
- return string.format("Overseer%s", key)
- end,
-})
-
-local function render_node(lines, highlights, node, depth, trace)
- local name = string.format("%s%s", string.rep(" ", depth), node.name)
- if trace[node.id] then
- local col = string.len(name) + 1
- for _, status in ipairs(trace[node.id]) do
- table.insert(
- highlights,
- { parser_status_to_hl[status], #lines + 1, col, col + string.len(status) }
- )
- col = col + string.len(status) + 1
- end
- name = name .. " " .. table.concat(trace[node.id], " ")
- end
- table.insert(lines, name)
- if node.child then
- render_node(lines, highlights, node.child, depth + 1, trace)
- elseif node.children then
- for _, child in ipairs(node.children) do
- render_node(lines, highlights, child, depth + 1, trace)
- end
- end
-end
-
-local function render_parser(input_lnum)
- local bufnr = output_buf
- if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then
- return
- end
- local ns = vim.api.nvim_create_namespace("OverseerParser")
- vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
- local p, err = load_parser()
- if not p then
- vim.bo[bufnr].modifiable = true
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, vim.split(vim.inspect(err), "\n"))
- vim.bo[bufnr].modifiable = false
- return
- end
- p:ingest(vim.api.nvim_buf_get_lines(input_buf, 0, input_lnum or -1, true))
- local trace = parser.get_trace()
- local lines = {}
- local highlights = {}
- if p.tree then
- render_node(lines, highlights, p.tree, 0, trace)
- elseif p.children then
- for k, v in pairs(p.children) do
- table.insert(lines, string.format("%s:", k))
- render_node(lines, highlights, v, 1, trace)
- end
- end
-
- local rem = p:get_remainder()
- if rem then
- table.insert(lines, "ITEM:")
- table.insert(highlights, { "Title", #lines, 0, -1 })
- vim.list_extend(lines, vim.split(vim.inspect(rem), "\n"))
- end
- table.insert(lines, "RESULT:")
- table.insert(highlights, { "Title", #lines, 0, -1 })
- local results = p:get_result()
- vim.list_extend(lines, vim.split(vim.inspect(results), "\n"))
-
- vim.bo[bufnr].modifiable = true
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
- vim.bo[bufnr].modifiable = false
- util.add_highlights(bufnr, ns, highlights)
-end
-
-local function create_source_bufnr()
- local file = get_filepath("debug_parser_source.lua")
- vim.cmd(string.format("edit %s", file))
- local bufnr = vim.api.nvim_get_current_buf()
- if vim.api.nvim_buf_line_count(bufnr) == 1 then
- local lines = {
- 'local parser = require("overseer.parser")',
- "return parser.new({",
- ' {"extract", "(.*)", "text"}',
- "})",
- }
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
- vim.cmd([[noautocmd write]])
- end
- source_buf = bufnr
- vim.api.nvim_create_autocmd("BufWritePost", {
- desc = "update parser debug view on write",
- callback = function()
- local lnum
- for _, winid in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
- if vim.api.nvim_win_is_valid(winid) and vim.api.nvim_win_get_buf(winid) == input_buf then
- lnum = vim.api.nvim_win_get_cursor(winid)[1]
- break
- end
- end
- render_parser(lnum)
- end,
- buffer = bufnr,
- })
-end
-
-local function create_output_buf(bufnr)
- if not bufnr then
- bufnr = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_win_set_buf(0, bufnr)
- end
- output_buf = bufnr
- vim.bo[bufnr].bufhidden = "wipe"
- vim.bo[bufnr].modifiable = false
-end
-
-local function create_input_buf()
- local file = get_filepath("debug_parser_input.txt")
- vim.cmd(string.format("edit %s", file))
- local bufnr = vim.api.nvim_get_current_buf()
- input_buf = bufnr
- if vim.api.nvim_buf_line_count(bufnr) == 1 then
- local lines = { "foo.lua:234: Sample input line" }
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
- vim.cmd([[noautocmd write]])
- end
- vim.api.nvim_create_autocmd("CursorMoved", {
- desc = "Rerun parser when cursor moves",
- buffer = bufnr,
- callback = function()
- local lnum = vim.api.nvim_win_get_cursor(0)[1]
- render_parser(lnum)
- end,
- })
-end
-
-M.start_debug_session = function()
- for _, buf in ipairs({ source_buf, output_buf, input_buf }) do
- if buf and vim.api.nvim_buf_is_valid(buf) then
- vim.api.nvim_buf_delete(buf, { force = true })
- end
- end
- local overseer_dir = files.join(vim.fn.stdpath("cache"), "overseer")
- if vim.fn.isdirectory(overseer_dir) == 0 then
- vim.fn.mkdir(overseer_dir)
- end
- vim.cmd([[tabnew]])
- create_source_bufnr()
- local source_win = vim.api.nvim_get_current_win()
- vim.cmd([[vsplit]])
- create_output_buf()
- vim.api.nvim_set_current_win(source_win)
- vim.cmd([[split]])
- create_input_buf()
- vim.api.nvim_set_current_win(source_win)
- render_parser()
-end
-
-return M
diff --git a/lua/overseer/parser/dispatch.lua b/lua/overseer/parser/dispatch.lua
deleted file mode 100644
index 9b00b677..00000000
--- a/lua/overseer/parser/dispatch.lua
+++ /dev/null
@@ -1,52 +0,0 @@
-local parser = require("overseer.parser")
-local Dispatch = {
- desc = "Dispatch an event",
- long_desc = "Events can be subscribed to using the parser:subscribe() method.",
- doc_args = {
- {
- name = "name",
- type = "string",
- desc = "Event name",
- },
- {
- name = "arg",
- type = "any|fun()",
- desc = "A value to send with the event, or a function that creates a value",
- vararg = true,
- },
- },
- examples = {
- {
- desc = [[clear_results will clear all current results from the parser. Pass `true` to only clear the results under the current key]],
- code = [[{"dispatch", "clear_results"}]],
- },
- {
- desc = [[set_results is used by the on_output_parse component to immediately set the current results on the task]],
- code = [[{"dispatch", "set_results"}]],
- },
- },
-}
-
-function Dispatch.new(name, ...)
- return setmetatable({
- event_name = name,
- args = { ... },
- }, { __index = Dispatch })
-end
-
-function Dispatch:reset() end
-
-function Dispatch:ingest(line, ctx)
- local params = {}
- for _, v in ipairs(self.args) do
- if type(v) == "function" then
- table.insert(params, v(line, ctx))
- else
- table.insert(params, v)
- end
- end
- ctx.dispatch(self.event_name, unpack(params))
- return parser.STATUS.SUCCESS
-end
-
-return Dispatch
diff --git a/lua/overseer/parser/ensure.lua b/lua/overseer/parser/ensure.lua
deleted file mode 100644
index 6b61e3fc..00000000
--- a/lua/overseer/parser/ensure.lua
+++ /dev/null
@@ -1,62 +0,0 @@
-local parser = require("overseer.parser")
-local util = require("overseer.parser.util")
-local Ensure = {
- desc = "Decorator that runs a child until it succeeds",
- doc_args = {
- {
- name = "succeed",
- type = "boolean",
- desc = "Set to false to run child until failure",
- default = true,
- position_optional = true,
- },
- {
- name = "child",
- type = "parser",
- desc = "The child parser node",
- },
- },
- examples = {
- {
- desc = [[An extract node that runs until it successfully parses]],
- code = [[
- {"ensure",
- {"extract", "^([^%s].+):(%d+): (.+)$", "filename", "lnum", "text" }
- }
-]],
- },
- },
-}
-
-local MAX_LOOP = 2
-
-function Ensure.new(succeed, child)
- if type(succeed) ~= "boolean" then
- child = succeed
- succeed = true
- end
- return setmetatable({
- child = util.hydrate(child),
- succeed = succeed,
- }, { __index = Ensure })
-end
-
-function Ensure:reset()
- self.child:reset()
-end
-
-function Ensure:ingest(...)
- for _ = 1, MAX_LOOP do
- local st = self.child:ingest(...)
- if st == parser.STATUS.FAILURE and self.succeed then
- self.child:reset()
- elseif st == parser.STATUS.SUCCESS and not self.succeed then
- self.child:reset()
- else
- return st
- end
- end
- return parser.STATUS.RUNNING
-end
-
-return Ensure
diff --git a/lua/overseer/parser/extract.lua b/lua/overseer/parser/extract.lua
deleted file mode 100644
index a08cca7b..00000000
--- a/lua/overseer/parser/extract.lua
+++ /dev/null
@@ -1,115 +0,0 @@
-local parser = require("overseer.parser")
-
-local Extract = {
- desc = "Parse a line into an object and append it to the results",
- doc_args = {
- {
- name = "opts",
- type = "object",
- desc = "Configuration options",
- position_optional = true,
- fields = {
- {
- name = "consume",
- type = "boolean",
- desc = "Consumes the line of input, blocking execution until the next line is fed in",
- default = true,
- },
- {
- name = "append",
- type = "boolean",
- desc = "After parsing, append the item to the results list. When false, the pending item will stick around.",
- default = true,
- },
- {
- name = "regex",
- type = "boolean",
- desc = "Use vim regex instead of lua pattern (see :help pattern)",
- default = false,
- },
- {
- name = "postprocess",
- type = "function",
- desc = "Call this function to do post-extraction processing on the values",
- },
- },
- },
- {
- name = "pattern",
- type = "string|function|string[]",
- desc = "The lua pattern to use for matching. Must have the same number of capture groups as there are field arguments.",
- long_desc = "Can also be a list of strings/functions and it will try matching against all of them",
- },
- {
- name = "field",
- type = "string",
- vararg = true,
- desc = 'The name of the extracted capture group. Use `"_"` to discard.',
- },
- },
- examples = {
- {
- desc = [[Convert a line in the format of `/path/to/file.txt:123: This is a message` into an item `{filename = "/path/to/file.txt", lnum = 123, text = "This is a message"}`]],
- code = [[{"extract", "^([^%s].+):(%d+): (.+)$", "filename", "lnum", "text" }]],
- },
- {
- desc = [[The same logic, but using a vim regex]],
- code = [[{"extract", {regex = true}, "\\v^([^:space:].+):(\\d+): (.+)$", "filename", "lnum", "text" }]],
- },
- },
-}
-
-function Extract.new(opts, pattern, ...)
- local fields
- if type(opts) ~= "table" then
- fields = { pattern, ... }
- pattern = opts
- opts = {}
- else
- fields = { ... }
- end
- opts = vim.tbl_deep_extend("keep", opts, {
- consume = true,
- append = true,
- regex = false,
- })
- return setmetatable({
- consume = opts.consume,
- append = opts.append,
- postprocess = opts.postprocess,
- done = nil,
- pattern = pattern,
- extract = parser.util.patterns_to_extract(pattern, opts.regex, fields),
- }, { __index = Extract })
-end
-
-function Extract:reset()
- self.done = nil
-end
-
-function Extract:ingest(line, ctx)
- if self.done then
- return self.done
- end
-
- local item = self.extract(line)
- if item then
- for k, v in pairs(item) do
- ctx.item[k] = v
- end
- vim.tbl_extend("force", ctx.item, item)
- end
-
- if not item then
- self.done = parser.STATUS.FAILURE
- return parser.STATUS.FAILURE
- end
- if self.postprocess then
- self.postprocess(ctx.item, ctx)
- end
- parser.util.append_item(self.append, line, ctx)
- self.done = parser.STATUS.SUCCESS
- return self.consume and parser.STATUS.RUNNING or parser.STATUS.SUCCESS
-end
-
-return Extract
diff --git a/lua/overseer/parser/extract_efm.lua b/lua/overseer/parser/extract_efm.lua
deleted file mode 100644
index 35c33658..00000000
--- a/lua/overseer/parser/extract_efm.lua
+++ /dev/null
@@ -1,124 +0,0 @@
-local parser = require("overseer.parser")
-local ExtractEfm = {
- desc = "Parse a line using vim's errorformat and append it to the results",
- doc_args = {
- {
- name = "opts",
- type = "object",
- desc = "Configuration options",
- position_optional = true,
- fields = {
- {
- name = "efm",
- type = "string",
- desc = "The errorformat string to use. Defaults to current option value.",
- },
- {
- name = "consume",
- type = "boolean",
- desc = "Consumes the line of input, blocking execution until the next line is fed in",
- default = true,
- },
- {
- name = "append",
- type = "boolean",
- desc = "After parsing, append the item to the results list. When false, the pending item will stick around.",
- default = true,
- },
- {
- name = "test",
- type = "function",
- desc = "A function that operates on the parsed value and returns true/false for SUCCESS/FAILURE",
- },
- {
- name = "postprocess",
- type = "function",
- desc = "Call this function to do post-extraction processing on the values",
- },
- },
- },
- },
-}
-
-function ExtractEfm.new(opts)
- opts = opts or {}
- opts = vim.tbl_deep_extend("keep", opts, {
- append = true,
- consume = true,
- })
- return setmetatable({
- efm = opts.efm,
- append = opts.append,
- consume = opts.consume,
- test = opts.test,
- postprocess = opts.postprocess,
- done = nil,
- }, { __index = ExtractEfm })
-end
-
-function ExtractEfm:reset()
- self.done = nil
-end
-
-function ExtractEfm:ingest(line, ctx)
- if self.done then
- return self.done
- end
- local item = ctx.item
-
- local parsed_item = vim.fn.getqflist({
- lines = { line },
- efm = self.efm,
- }).items[1]
- if not parsed_item or parsed_item.valid ~= 1 or (self.test and not self.test(parsed_item)) then
- self.done = parser.STATUS.FAILURE
- return parser.STATUS.FAILURE
- end
-
- -- Convert the quickfix item format to something a little easier to process
- if not parsed_item.filename and parsed_item.bufnr ~= 0 then
- parsed_item.filename = vim.api.nvim_buf_get_name(parsed_item.bufnr)
- end
- parsed_item.bufnr = nil
- parsed_item.valid = nil
- if parsed_item.module == "" then
- parsed_item.module = nil
- end
- if parsed_item.nr == -1 then
- parsed_item.nr = nil
- end
- if parsed_item.type == "" then
- parsed_item.type = nil
- end
- if parsed_item.pattern == "" then
- parsed_item.pattern = nil
- end
- if parsed_item.col == 0 then
- parsed_item.col = nil
- end
- if parsed_item.vcol == 0 then
- parsed_item.vcol = nil
- end
- if parsed_item.end_col == 0 then
- parsed_item.end_col = nil
- end
- if parsed_item.lnum == 0 then
- parsed_item.lnum = nil
- end
- if parsed_item.end_lnum == 0 then
- parsed_item.end_lnum = nil
- end
-
- for k, v in pairs(parsed_item) do
- item[k] = v
- end
-
- if self.postprocess then
- self.postprocess(item, { line = line })
- end
- parser.util.append_item(self.append, line, ctx)
- self.done = parser.STATUS.SUCCESS
- return self.consume and parser.STATUS.RUNNING or parser.STATUS.SUCCESS
-end
-
-return ExtractEfm
diff --git a/lua/overseer/parser/extract_json.lua b/lua/overseer/parser/extract_json.lua
deleted file mode 100644
index 50d937f7..00000000
--- a/lua/overseer/parser/extract_json.lua
+++ /dev/null
@@ -1,82 +0,0 @@
-local parser = require("overseer.parser")
-local ExtractJson = {
- desc = "Parse a line as json and append it to the results",
- doc_args = {
- {
- name = "opts",
- type = "object",
- desc = "Configuration options",
- position_optional = true,
- fields = {
- {
- name = "consume",
- type = "boolean",
- desc = "Consumes the line of input, blocking execution until the next line is fed in",
- default = true,
- },
- {
- name = "append",
- type = "boolean",
- desc = "After parsing, append the item to the results list. When false, the pending item will stick around.",
- default = true,
- },
- {
- name = "test",
- type = "function",
- desc = "A function that operates on the parsed value and returns true/false for SUCCESS/FAILURE",
- },
- {
- name = "postprocess",
- type = "function",
- desc = "Call this function to do post-extraction processing on the values",
- },
- },
- },
- },
-}
-
-function ExtractJson.new(opts)
- opts = opts or {}
- opts = vim.tbl_deep_extend("keep", opts, {
- append = true,
- consume = true,
- })
- return setmetatable({
- append = opts.append,
- consume = opts.consume,
- test = opts.test,
- postprocess = opts.postprocess,
- done = nil,
- }, { __index = ExtractJson })
-end
-
-function ExtractJson:reset()
- self.done = nil
-end
-
-function ExtractJson:ingest(line, ctx)
- if self.done then
- return self.done
- end
- local item = ctx.item
-
- local ok, result = pcall(vim.json.decode, line, { luanil = { object = true } })
- if not ok or (self.test and not self.test(result)) then
- self.done = parser.STATUS.FAILURE
- return parser.STATUS.FAILURE
- end
- assert(result)
-
- for k, v in pairs(result) do
- item[k] = v
- end
-
- if self.postprocess then
- self.postprocess(item, { line = line })
- end
- parser.util.append_item(self.append, line, ctx)
- self.done = parser.STATUS.SUCCESS
- return self.consume and parser.STATUS.RUNNING or parser.STATUS.SUCCESS
-end
-
-return ExtractJson
diff --git a/lua/overseer/parser/extract_multiline.lua b/lua/overseer/parser/extract_multiline.lua
deleted file mode 100644
index 5517f96f..00000000
--- a/lua/overseer/parser/extract_multiline.lua
+++ /dev/null
@@ -1,101 +0,0 @@
-local parser = require("overseer.parser")
-local ExtractMultiline = {
- desc = "Extract a multiline string as a single field on an item",
- doc_args = {
- {
- name = "opts",
- type = "object",
- desc = "Configuration options",
- position_optional = true,
- fields = {
- {
- name = "append",
- type = "boolean",
- desc = "After parsing, append the item to the results list. When false, the pending item will stick around.",
- default = true,
- },
- },
- },
- {
- name = "pattern",
- type = "string|function",
- desc = "The lua pattern to use for matching. As long as the pattern matches, lines will continue to be appended to the field.",
- },
- {
- name = "field",
- type = "string",
- desc = "The name of the field to add to the item",
- },
- },
- examples = {
- {
- desc = [[Extract all indented lines as a message]],
- code = [[{"extract_multiline", "^( .+)", "message"}]],
- },
- },
-}
-
-function ExtractMultiline.new(opts, pattern, field)
- if field == nil then
- field = pattern
- pattern = opts
- opts = {}
- end
- opts = vim.tbl_deep_extend("keep", opts, {
- append = true,
- })
- return setmetatable({
- append = opts.append,
- done = nil,
- any_match = false,
- pattern = pattern,
- field = field,
- }, { __index = ExtractMultiline })
-end
-
-function ExtractMultiline:reset()
- self.done = nil
- self.any_match = false
-end
-
-local function append_line(item, key, value)
- if not item[key] then
- item[key] = value
- else
- item[key] = item[key] .. "\n" .. value
- end
-end
-
-function ExtractMultiline:ingest(line, ctx)
- if self.done then
- return self.done
- end
- local item = ctx.item
-
- local result
- if type(self.pattern) == "string" then
- result = line:match(self.pattern)
- else
- result = self.pattern(line)
- end
- if result then
- self.any_match = true
- if type(self.field) == "table" then
- local key, postprocess = unpack(self.field)
- append_line(item, key, postprocess(result, self))
- else
- append_line(item, self.field, result)
- end
- return parser.STATUS.RUNNING
- else
- if self.any_match or not vim.tbl_isempty(item) then
- self.done = parser.STATUS.SUCCESS
- parser.util.append_item(self.append, line, ctx)
- else
- self.done = parser.STATUS.FAILURE
- end
- return self.done
- end
-end
-
-return ExtractMultiline
diff --git a/lua/overseer/parser/extract_nested.lua b/lua/overseer/parser/extract_nested.lua
deleted file mode 100644
index d303363f..00000000
--- a/lua/overseer/parser/extract_nested.lua
+++ /dev/null
@@ -1,128 +0,0 @@
-local parser = require("overseer.parser")
-local util = require("overseer.parser.util")
-local ExtractNested = {
- desc = "Run a subparser and put the extracted results on the field of an item",
- doc_args = {
- {
- name = "opts",
- type = "object",
- desc = "Configuration options",
- position_optional = true,
- fields = {
- {
- name = "append",
- type = "boolean",
- desc = "After parsing, append the item to the results list. When false, the pending item will stick around.",
- default = true,
- },
- {
- name = "fail_on_empty",
- type = "boolean",
- desc = "Return FAILURE if there are no results from the child",
- default = true,
- },
- },
- },
- {
- name = "field",
- type = "string",
- desc = "The name of the field to add to the item",
- },
- {
- name = "child",
- type = "parser",
- desc = "The child parser node",
- },
- },
- examples = {
- {
- desc = [[Extract a golang test failure, then add the stacktrace to it (if present)]],
- code = [[
- {"extract",
- {
- regex = true,
- append = false,
- },
- "\\v^--- (FAIL|PASS|SKIP): ([^[:space:] ]+) \\(([0-9\\.]+)s\\)",
- "status",
- "name",
- "duration",
- },
- {"always",
- {"sequence",
- {"test", "^panic:"},
- {"skip_until", "^goroutine%s"},
- {"extract_nested",
- { append = false },
- "stacktrace",
- {"loop",
- {"sequence",
- {"extract",{ append = false }, { "^(.+)%(.*%)$", "^created by (.+)$" }, "text"},
- {"extract","^%s+([^:]+.go):([0-9]+)", "filename", "lnum"}
- }
- }
- }
- }
- }
- ]],
- },
- },
-}
-
-function ExtractNested.new(opts, field, child)
- if child == nil then
- child = field
- field = opts
- opts = {}
- end
- opts = vim.tbl_deep_extend("keep", opts, {
- append = true,
- fail_on_empty = true,
- })
- return setmetatable({
- child = util.hydrate(child),
- field = field,
- append = opts.append,
- fail_on_empty = opts.fail_on_empty,
- results = {},
- item = {},
- }, { __index = ExtractNested })
-end
-
-function ExtractNested:reset()
- self.done = nil
- self.results = {}
- self.item = {}
- self.child:reset()
-end
-
-function ExtractNested:ingest(line, ctx)
- if self.done then
- return self.done
- end
- local nested_ctx = {
- results = self.results,
- item = self.item,
- }
- local st = self.child:ingest(line, nested_ctx)
- if st == parser.STATUS.FAILURE then
- if not self.fail_on_empty or not vim.tbl_isempty(self.results) then
- st = parser.STATUS.SUCCESS
- end
- elseif st == parser.STATUS.RUNNING then
- if not vim.tbl_isempty(self.results) then
- -- As soon as we extract any values, make sure the field exists on the item
- ctx.item[self.field] = self.results
- end
- return st
- end
-
- if st == parser.STATUS.SUCCESS then
- ctx.item[self.field] = self.results
- parser.util.append_item(self.append, line, ctx)
- end
-
- self.done = st
- return self.done
-end
-return ExtractNested
diff --git a/lua/overseer/parser/init.lua b/lua/overseer/parser/init.lua
deleted file mode 100644
index 0103077a..00000000
--- a/lua/overseer/parser/init.lua
+++ /dev/null
@@ -1,324 +0,0 @@
--- Utilities for parsing lines of output
-local Enum = require("overseer.enum")
-local util = require("overseer.util")
----@diagnostic disable-next-line: deprecated
-local islist = vim.islist or vim.tbl_islist
-local M = {}
-
-local debug = false
-local next_id = 1
----@type table
-local trace = {}
-
----@param id integer
----@param action overseer.ParserStatus
-local function add_trace(id, action)
- if not trace[id] then
- trace[id] = { action }
- else
- table.insert(trace[id], action)
- end
-end
-
----@class overseer.Parser
----@field reset fun(self: overseer.Parser)
----@field ingest fun(self: overseer.Parser, lines: string[]): overseer.ParserStatus
----@field subscribe fun(self: overseer.Parser, event: string, callback: fun(key: string, value: any))
----@field unsubscribe fun(self: overseer.Parser, event: string, callback: fun(key: string, value: any))
----@field get_result fun(self: overseer.Parser): table
----@field get_remainder fun(self: overseer.Parser): table
----@note
---- Built-in events that can be subscribed to:
---- new_item Dispatched when an item is appended to the result
---- clear_results Clear results items from the parser
---- set_results Canonically used to force the on_output_parse component to set task results
-
----@class overseer.ParserNode
----@field ingest fun(self: overseer.ParserNode, line: string, ctx: table): overseer.ParserStatus
----@field reset fun()
-
-setmetatable(M, {
- __index = function(_, key)
- local mod = require(string.format("overseer.parser.%s", key))
- if key == "util" or key == "debug" then
- return mod
- end
- if debug then
- return function(...)
- local node = mod.new(...)
- local ingest = node.ingest
- local reset = node.reset
- node.reset = function(self)
- add_trace(self.id, "RESET")
- reset(self)
- end
- node.ingest = function(self, line, ctx)
- local depth = ctx.debug_depth or 0
- ctx.debug_depth = depth + 1
- local st = ingest(self, line, ctx)
- add_trace(self.id, st)
- ctx.debug_depth = depth
- return st
- end
- node.id = next_id
- node.name = key
- next_id = next_id + 1
- return node
- end
- else
- return mod.new
- end
- end,
-})
-
----@alias overseer.ParserStatus "RESET"|"RUNNING"|"SUCCESS"|"FAILURE"
-
-M.STATUS = Enum.new({
- "RUNNING",
- "SUCCESS",
- "FAILURE",
-})
-
----@param subs table
----@param event string
----@param callback fun()
-local function subscribe(subs, event, callback)
- if not subs[event] then
- subs[event] = {}
- end
- table.insert(subs[event], callback)
-end
-
----@param subs table
----@param event string
----@param callback fun()
-local function unsubscribe(subs, event, callback)
- if subs[event] then
- util.tbl_remove(subs[event], callback)
- end
-end
-
-local function dispatch(subs, event, ...)
- if subs[event] then
- for _, cb in ipairs(subs[event]) do
- cb(...)
- end
- end
-end
-
----@class overseer.ListParser : overseer.Parser
----@field tree overseer.ParserNode
----@field subs table
-local ListParser = {}
-
----@return overseer.ListParser
-function ListParser.new(children)
- local parser = {
- tree = M.loop({ ignore_failure = true }, M.sequence(children)),
- results = {},
- item = {},
- subs = {},
- }
- setmetatable(parser, { __index = ListParser })
- parser:subscribe("clear_results", function()
- parser.results = {}
- if parser.ctx then
- parser.ctx.results = parser.results
- parser.ctx.__num_results = 0
- end
- end)
- return parser
-end
-
-function ListParser:reset()
- self.tree:reset()
- self.results = {}
- self.item = {}
-end
-
-function ListParser:ingest(lines)
- self.ctx = {
- __num_results = #self.results,
- item = self.item,
- results = self.results,
- default_values = {},
- dispatch = function(...)
- dispatch(self.subs, ...)
- end,
- }
- for _, line in ipairs(lines) do
- self.ctx.line = line
- if debug then
- trace = {}
- end
- self.tree:ingest(line, self.ctx)
- end
- for i = self.ctx.__num_results + 1, #self.results do
- local result = self.results[i]
- dispatch(self.subs, "new_item", "", result)
- end
- self.ctx = nil
-end
-
-function ListParser:subscribe(event, callback)
- subscribe(self.subs, event, callback)
-end
-
-function ListParser:unsubscribe(event, callback)
- unsubscribe(self.subs, event, callback)
-end
-
-function ListParser:get_result()
- return self.results
-end
-
-function ListParser:get_remainder()
- if not vim.tbl_isempty(self.item) then
- return self.item
- end
-end
-
----@class overseer.MapParser : overseer.Parser
----@field children table
----@field results table
----@field items table
----@field subs table
-local MapParser = {}
-
----@return overseer.MapParser
-function MapParser.new(children)
- local results = {}
- local items = {}
- local wrapped_children = {}
- for k, v in pairs(children) do
- results[k] = {}
- items[k] = {}
- wrapped_children[k] = M.loop({ ignore_failure = true }, M.sequence(v))
- end
- local parser = {
- children = wrapped_children,
- results = results,
- items = items,
- subs = {},
- }
- setmetatable(parser, { __index = MapParser })
- parser:subscribe("clear_results", function(current_key_only)
- if not current_key_only then
- for k in pairs(parser.children) do
- parser.results[k] = {}
- end
- if parser.ctx then
- parser.ctx.results = parser.results[parser.ctx.__key]
- parser.ctx.__num_results = 0
- end
- elseif parser.ctx then
- -- We want to clear just the items for the current key in results, so we need to modify the
- -- ctx results in-place
- while not vim.tbl_isempty(parser.ctx.results) do
- table.remove(parser.ctx.results)
- end
- parser.ctx.__num_results = 0
- end
- end)
- return parser
-end
-
-function MapParser:reset()
- for k, v in pairs(self.children) do
- self.results[k] = {}
- self.items[k] = {}
- v:reset()
- end
-end
-
-function MapParser:ingest(lines)
- for _, line in ipairs(lines) do
- if debug then
- trace = {}
- end
- for k, v in pairs(self.children) do
- self.ctx = {
- __key = k,
- __num_results = #self.results[k],
- item = self.items[k],
- results = self.results[k],
- default_values = {},
- line = line,
- dispatch = function(...)
- dispatch(self.subs, ...)
- end,
- }
- v:ingest(line, self.ctx)
- for i = self.ctx.__num_results + 1, #self.ctx.results do
- local result = self.ctx.results[i]
- dispatch(self.subs, "new_item", k, result)
- end
- self.ctx = nil
- end
- end
-end
-
-function MapParser:subscribe(event, callback)
- subscribe(self.subs, event, callback)
-end
-
-function MapParser:unsubscribe(event, callback)
- unsubscribe(self.subs, event, callback)
-end
-
-function MapParser:get_result()
- return self.results
-end
-
-function MapParser:get_remainder()
- for _, v in pairs(self.items) do
- if not vim.tbl_isempty(v) then
- return self.items
- end
- end
-end
-
----@param config table
----@return overseer.Parser
-M.new = function(config)
- vim.validate({
- config = { config, "t" },
- })
- if islist(config) or M.util.is_parser(config) then
- return ListParser.new(config)
- else
- return MapParser.new(config)
- end
-end
-
----@param enabled boolean
-M.trace = function(enabled)
- debug = enabled
-end
-
----@return table
-M.get_trace = function()
- return trace
-end
-
----Used for documentation generation
----@private
-M.get_parser_docs = function(...)
- local ret = {}
- for _, name in ipairs({ ... }) do
- local mod = require(string.format("overseer.parser.%s", name))
- if mod.doc_args then
- table.insert(ret, {
- name = name,
- desc = mod.desc,
- doc_args = mod.doc_args,
- examples = mod.examples,
- })
- else
- table.insert(ret, {})
- end
- end
- return ret
-end
-
-return M
diff --git a/lua/overseer/parser/inline.lua b/lua/overseer/parser/inline.lua
deleted file mode 100644
index a51fd844..00000000
--- a/lua/overseer/parser/inline.lua
+++ /dev/null
@@ -1,20 +0,0 @@
-local Inline = {}
-
-function Inline.new(callback, reset)
- return setmetatable({
- callback = callback,
- reset_fn = reset,
- }, { __index = Inline })
-end
-
-function Inline:reset()
- if self.reset_fn then
- self.reset_fn()
- end
-end
-
-function Inline:ingest(...)
- return self.callback(...)
-end
-
-return Inline
diff --git a/lua/overseer/parser/invert.lua b/lua/overseer/parser/invert.lua
deleted file mode 100644
index 621516a8..00000000
--- a/lua/overseer/parser/invert.lua
+++ /dev/null
@@ -1,45 +0,0 @@
-local parser = require("overseer.parser")
-local util = require("overseer.parser.util")
-local Invert = {
- desc = "A decorator that inverts the child's return value",
- doc_args = {
- {
- name = "child",
- type = "parser",
- desc = "The child parser node",
- },
- },
- examples = {
- {
- desc = [[An extract node that returns SUCCESS when it fails, and vice-versa]],
- code = [[
- {"invert",
- {"extract", "^([^%s].+):(%d+): (.+)$", "filename", "lnum", "text" }
- }
-]],
- },
- },
-}
-
-function Invert.new(child)
- return setmetatable({
- child = util.hydrate(child),
- }, { __index = Invert })
-end
-
-function Invert:reset()
- self.child:reset()
-end
-
-function Invert:ingest(...)
- local st = self.child:ingest(...)
- if st == parser.STATUS.FAILURE then
- return parser.STATUS.SUCCESS
- elseif st == parser.STATUS.SUCCESS then
- return parser.STATUS.FAILURE
- else
- return st
- end
-end
-
-return Invert
diff --git a/lua/overseer/parser/lib.lua b/lua/overseer/parser/lib.lua
deleted file mode 100644
index 932fa403..00000000
--- a/lua/overseer/parser/lib.lua
+++ /dev/null
@@ -1,71 +0,0 @@
-local M = {}
-
----Create a list of nodes that will parse repeating "watch" command output (e.g. tsc --watch)
----@param start_pat string|table Pattern or {opts, pattern} table that matches when we should start extracting
----@param end_pat string|table Pattern or {opts, pattern} table that matches when we should finish extracting
----@param opts table
---- wrap boolean If true, wrap the resulting parser in a loop->sequence
---- active_on_start boolean When false, require start_pat to match before parsing errors
---- only_clear_results_key boolean When true, only clear the current results key
----@return table
-M.watcher_output = function(start_pat, end_pat, extraction, opts)
- opts = vim.tbl_extend("keep", opts or {}, {
- wrap = false,
- active_on_start = true,
- only_clear_results_key = false,
- })
- local end_test
- if type(end_pat) == "table" then
- end_test = { "test", unpack(end_pat) }
- else
- end_test = { "test", end_pat }
- end
- local skip_until_start
- if type(start_pat) == "table" then
- local start_opts, pat = unpack(start_pat)
- start_opts.skip_matching_line = true
- skip_until_start = { "skip_until", start_opts, pat }
- else
- skip_until_start = { "skip_until", { skip_matching_line = true }, start_pat }
- end
- local seq = {
- {
- "always", -- When the loop exits, proceed to the next node
- {
- "loop", -- Extract errors until exit
- {
- "parallel",
- {
- "invert", -- Exit the loop when we detect the end of the output
- end_test,
- },
- {
- "always", -- Don't exit the loop if extraction fails
- extraction,
- },
- -- Prevent spin-looping when extraction fails
- { "skip_lines", 1 },
- },
- },
- },
- { "dispatch", "set_results" },
- }
- local reset_seq = {
- skip_until_start,
- { "dispatch", "clear_results", opts.only_clear_results_key },
- }
- if opts.active_on_start then
- vim.list_extend(seq, reset_seq)
- else
- vim.list_extend(reset_seq, seq)
- seq = reset_seq
- end
-
- if opts.wrap then
- table.insert(seq, 1, "sequence")
- seq = { "loop", seq }
- end
- return seq
-end
-
-return M
diff --git a/lua/overseer/parser/loop.lua b/lua/overseer/parser/loop.lua
deleted file mode 100644
index 0f2a24cf..00000000
--- a/lua/overseer/parser/loop.lua
+++ /dev/null
@@ -1,86 +0,0 @@
-local parser = require("overseer.parser")
-local util = require("overseer.parser.util")
-local Loop = {
- desc = "A decorator that repeats the child",
- doc_args = {
- {
- name = "opts",
- type = "object",
- desc = "Configuration options",
- position_optional = true,
- fields = {
- {
- name = "ignore_failure",
- type = "boolean",
- desc = "Keep looping even when the child fails",
- default = false,
- },
- {
- name = "repetitions",
- type = "integer",
- desc = "When set, loop a set number of times then return SUCCESS",
- },
- },
- },
- {
- name = "child",
- type = "parser",
- desc = "The child parser node",
- },
- },
-}
-
-local MAX_LOOP = 2
-
-function Loop.new(opts, child)
- if child == nil then
- child = opts
- opts = {}
- end
- vim.validate({
- ignore_failure = { opts.ignore_failure, "b", true },
- repetitions = { opts.repetitions, "n", true },
- })
- return setmetatable({
- ignore_failure = opts.ignore_failure,
- repetitions = opts.repetitions,
- count = 0,
- done = nil,
- child = util.hydrate(child),
- }, { __index = Loop })
-end
-
-function Loop:reset()
- self.count = 0
- self.done = nil
- self.child:reset()
-end
-
-function Loop:ingest(...)
- if self.done then
- return self.done
- end
- local loop_count = 0
- local st
- repeat
- if self.repetitions and self.count >= self.repetitions then
- return parser.STATUS.SUCCESS
- end
- st = self.child:ingest(...)
- if st == parser.STATUS.SUCCESS then
- self.child:reset()
- self.count = self.count + 1
- elseif st == parser.STATUS.FAILURE then
- self.child:reset()
- self.count = self.count + 1
- if not self.ignore_failure then
- self.done = st
- return st
- end
- end
- loop_count = loop_count + 1
- until st == parser.STATUS.RUNNING or loop_count >= MAX_LOOP
- return parser.STATUS.RUNNING
-end
-
-return Loop
diff --git a/lua/overseer/parser/parallel.lua b/lua/overseer/parser/parallel.lua
deleted file mode 100644
index b1158caa..00000000
--- a/lua/overseer/parser/parallel.lua
+++ /dev/null
@@ -1,105 +0,0 @@
-local parser = require("overseer.parser")
-local parser_util = require("overseer.parser.util")
-local Parallel = {
- desc = "Run the child nodes in parallel",
- long_desc = "The children are still run in-order, it just means that the input lines are fed to all the children on every iteration",
- doc_args = {
- {
- name = "opts",
- type = "object",
- desc = "Configuration options",
- position_optional = true,
- fields = {
- {
- name = "break_on_first_failure",
- type = "boolean",
- desc = "Stop executing as soon as a child returns FAILURE",
- default = true,
- },
- {
- name = "break_on_first_success",
- type = "boolean",
- desc = "Stop executing as soon as a child returns SUCCESS",
- default = false,
- },
- {
- name = "reset_children",
- type = "boolean",
- desc = "Reset all children at the beginning of each iteration",
- default = false,
- },
- },
- },
- {
- name = "child",
- type = "parser",
- vararg = true,
- desc = "The child parser nodes. Can be passed in as varargs or as a list.",
- },
- },
-}
-
-function Parallel.new(opts, ...)
- local children
- if parser_util.is_parser(opts) then
- children = vim.F.pack_len(opts, ...)
- opts = {}
- else
- children = vim.F.pack_len(...)
- end
- vim.validate({
- break_on_first_failure = { opts.break_on_first_failure, "b", true },
- break_on_first_success = { opts.break_on_first_success, "b", true },
- reset_children = { opts.reset_children, "b", true },
- })
- opts = vim.tbl_deep_extend("keep", opts, {
- break_on_first_failure = true,
- break_on_first_success = false,
- reset_children = false,
- })
- return setmetatable({
- break_on_first_success = opts.break_on_first_success,
- break_on_first_failure = opts.break_on_first_failure,
- reset_children = opts.reset_children,
- children = parser_util.hydrate_list(children),
- }, { __index = Parallel })
-end
-
-function Parallel:reset()
- for _, child in ipairs(self.children) do
- child:reset()
- end
-end
-
-function Parallel:ingest(...)
- local any_failed = false
- local any_running = false
- for _, child in ipairs(self.children) do
- if self.reset_children then
- child:reset()
- end
- local st = child:ingest(...)
- if st == parser.STATUS.SUCCESS then
- if self.break_on_first_success then
- return st
- end
- elseif st == parser.STATUS.FAILURE then
- if self.break_on_first_failure then
- return st
- end
- any_failed = true
- else
- any_running = true
- end
- end
-
- if any_running then
- return parser.STATUS.RUNNING
- elseif any_failed then
- return parser.STATUS.FAILURE
- else
- return parser.STATUS.SUCCESS
- end
-end
-
-return Parallel
diff --git a/lua/overseer/parser/sequence.lua b/lua/overseer/parser/sequence.lua
deleted file mode 100644
index 64c0cae2..00000000
--- a/lua/overseer/parser/sequence.lua
+++ /dev/null
@@ -1,120 +0,0 @@
-local parser = require("overseer.parser")
-local parser_util = require("overseer.parser.util")
-local Sequence = {
- desc = "Run the child nodes sequentially",
- doc_args = {
- {
- name = "opts",
- type = "object",
- desc = "Configuration options",
- position_optional = true,
- fields = {
- {
- name = "break_on_first_failure",
- type = "boolean",
- desc = "Stop executing as soon as a child returns FAILURE",
- default = true,
- },
- {
- name = "break_on_first_success",
- type = "boolean",
- desc = "Stop executing as soon as a child returns SUCCESS",
- default = false,
- },
- },
- },
- {
- name = "child",
- type = "parser",
- vararg = true,
- desc = "The child parser nodes. Can be passed in as varargs or as a list.",
- },
- },
- examples = {
- {
- desc = [[Extract the message text from one line, then the filename and lnum from the next line]],
- code = [[
- {"sequence",
- {"extract", { append = false }, { "^(.+)%(.*%)$", "^created by (.+)$" }, "text"},
- {"extract", "^%s+([^:]+.go):([0-9]+)", "filename", "lnum"}
- }
- ]],
- },
- },
-}
-
-function Sequence.new(opts, ...)
- local children
- if parser_util.is_parser(opts) then
- -- No opts, children passed in as args
- children = vim.F.pack_len(opts, ...)
- opts = {}
- elseif parser_util.tbl_is_parser_list(opts) then
- -- No opts, children are passed in as a list
- children = opts
- opts = {}
- else
- if select("#", ...) == 1 then
- local arg1 = select(1, ...)
- -- we got opts, and children are passed in as a list
- if parser_util.tbl_is_parser_list(arg1) then
- children = arg1
- end
- end
- if not children then
- -- children are passed in as args
- children = vim.F.pack_len(...)
- end
- end
- vim.validate({
- break_on_first_failure = { opts.break_on_first_failure, "b", true },
- break_on_first_success = { opts.break_on_first_success, "b", true },
- })
- opts = vim.tbl_deep_extend("keep", opts, {
- break_on_first_failure = true,
- break_on_first_success = false,
- })
- return setmetatable({
- idx = 1,
- any_failures = false,
- break_on_first_success = opts.break_on_first_success,
- break_on_first_failure = opts.break_on_first_failure,
- children = parser_util.hydrate_list(children),
- }, { __index = Sequence })
-end
-
-function Sequence:reset()
- self.idx = 1
- self.any_failures = false
- for _, child in ipairs(self.children) do
- child:reset()
- end
-end
-
-function Sequence:ingest(...)
- while self.idx <= #self.children do
- local child = self.children[self.idx]
- local st = child:ingest(...)
- if st == parser.STATUS.SUCCESS then
- if self.break_on_first_success then
- return st
- end
- elseif st == parser.STATUS.FAILURE then
- self.any_failures = true
- if self.break_on_first_failure then
- return st
- end
- elseif st == parser.STATUS.RUNNING then
- return st
- end
- self.idx = self.idx + 1
- end
-
- if self.any_failures then
- return parser.STATUS.FAILURE
- else
- return parser.STATUS.SUCCESS
- end
-end
-
-return Sequence
diff --git a/lua/overseer/parser/set_defaults.lua b/lua/overseer/parser/set_defaults.lua
deleted file mode 100644
index 0723fd84..00000000
--- a/lua/overseer/parser/set_defaults.lua
+++ /dev/null
@@ -1,87 +0,0 @@
-local util = require("overseer.parser.util")
-local SetDefaults = {
- desc = "A decorator that adds values to any items extracted by the child",
- doc_args = {
- {
- name = "opts",
- type = "object",
- desc = "Configuration options",
- position_optional = true,
- fields = {
- {
- name = "values",
- type = "object",
- desc = "Hardcoded key-value pairs to set as default values",
- },
- {
- name = "hoist_item",
- type = "boolean",
- desc = "Take the current pending item, and use its fields as the default key-value pairs",
- default = true,
- },
- },
- },
- {
- name = "child",
- type = "parser",
- desc = "The child parser node",
- },
- },
- examples = {
- {
- desc = [[Extract the filename from a header line, then for each line of output beneath it parse the test name + status, and also add the filename to each item]],
- code = [[
- {"sequence",
- {"extract", {append = false}, "^Test result (.+)$", "filename"}
- {"set_defaults",
- {"loop",
- {"extract", "^Test (.+): (.+)$", "test_name", "status"}
- }
- }
- }
- ]],
- },
- },
-}
-
-function SetDefaults.new(opts, child)
- if child == nil then
- child = opts
- opts = {}
- end
- opts = vim.tbl_deep_extend("keep", opts, {
- values = {},
- hoist_item = true,
- })
- vim.validate({
- values = { opts.values, "t" },
- hoist_item = { opts.hoist_item, "b" },
- })
- return setmetatable({
- default_values = opts.values,
- hoist_item = opts.hoist_item,
- current_defaults = nil,
- child = util.hydrate(child),
- }, { __index = SetDefaults })
-end
-
-function SetDefaults:reset()
- self.current_defaults = nil
- self.child:reset()
-end
-
-function SetDefaults:ingest(line, ctx)
- if not self.current_defaults then
- self.current_defaults = vim.deepcopy(self.default_values)
- if self.hoist_item then
- self.current_defaults = vim.tbl_extend("force", self.current_defaults, ctx.item)
- end
- end
- local prev_default_values = ctx.default_values
- ctx.default_values = vim.tbl_deep_extend("force", ctx.default_values or {}, self.current_defaults)
- local status = self.child:ingest(line, ctx)
- ctx.default_values = prev_default_values
- return status
-end
-
-return SetDefaults
diff --git a/lua/overseer/parser/skip_lines.lua b/lua/overseer/parser/skip_lines.lua
deleted file mode 100644
index c9ba9f41..00000000
--- a/lua/overseer/parser/skip_lines.lua
+++ /dev/null
@@ -1,30 +0,0 @@
-local parser = require("overseer.parser")
-local SkipLines = {
- desc = "Skip over a set number of lines",
- doc_args = {
- {
- name = "count",
- type = "integer",
- desc = "How many lines to skip",
- },
- },
-}
-
-function SkipLines.new(count)
- return setmetatable({ count = count, idx = 0 }, { __index = SkipLines })
-end
-
-function SkipLines:reset()
- self.idx = 0
-end
-
-function SkipLines:ingest()
- self.idx = self.idx + 1
- if self.idx <= self.count then
- return parser.STATUS.RUNNING
- else
- return parser.STATUS.SUCCESS
- end
-end
-
-return SkipLines
diff --git a/lua/overseer/parser/skip_until.lua b/lua/overseer/parser/skip_until.lua
deleted file mode 100644
index 313de9a8..00000000
--- a/lua/overseer/parser/skip_until.lua
+++ /dev/null
@@ -1,80 +0,0 @@
-local parser = require("overseer.parser")
-local SkipUntil = {
- desc = "Skip over lines until one matches",
- doc_args = {
- {
- name = "opts",
- type = "object",
- desc = "Configuration options",
- position_optional = true,
- fields = {
- {
- name = "skip_matching_line",
- type = "boolean",
- desc = "Consumes the line that matches. Later nodes will only see the next line.",
- default = true,
- },
- {
- name = "regex",
- type = "boolean",
- desc = "Use vim regex instead of lua pattern (see :help pattern)",
- default = true,
- },
- },
- },
- {
- name = "pattern",
- vararg = true,
- type = "string|string[]|fun(line: string): string",
- desc = "The lua pattern to use for matching. The node succeeds if any of these patterns match.",
- },
- },
- examples = {
- {
- desc = [[Skip input until we see "Error" or "Warning"]],
- code = [[{"skip_until", "^Error:", "^Warning:"}]],
- },
- },
-}
-
-function SkipUntil.new(opts, ...)
- local patterns
- if type(opts) ~= "table" then
- patterns = { opts, ... }
- opts = {}
- else
- patterns = { ... }
- end
- vim.validate({
- skip_matching_line = { opts.skip_matching_line, "b", true },
- })
- if opts.skip_matching_line == nil then
- opts.skip_matching_line = true
- end
- return setmetatable({
- skip_matching_line = opts.skip_matching_line,
- test = parser.util.patterns_to_test(patterns, opts.regex),
- done = false,
- }, { __index = SkipUntil })
-end
-
-function SkipUntil:reset()
- self.done = false
-end
-
-function SkipUntil:ingest(line)
- if self.done then
- return parser.STATUS.SUCCESS
- end
- if self.test(line) then
- self.done = true
- if self.skip_matching_line then
- return parser.STATUS.RUNNING
- else
- return parser.STATUS.SUCCESS
- end
- end
- return parser.STATUS.RUNNING
-end
-
-return SkipUntil
diff --git a/lua/overseer/parser/test.lua b/lua/overseer/parser/test.lua
deleted file mode 100644
index 70ca155a..00000000
--- a/lua/overseer/parser/test.lua
+++ /dev/null
@@ -1,53 +0,0 @@
-local parser = require("overseer.parser")
-local Test = {
- desc = "Returns SUCCESS when the line matches the pattern",
- doc_args = {
- {
- name = "opts",
- type = "object",
- desc = "Configuration options",
- position_optional = true,
- fields = {
- {
- name = "regex",
- type = "boolean",
- desc = "Use vim regex instead of lua pattern (see :help pattern)",
- default = true,
- },
- },
- },
- {
- name = "pattern",
- type = "string|fun(line: string): string",
- desc = "The lua pattern to use for matching, or test function",
- },
- },
- examples = {
- {
- desc = [[Fail until a line starts with "panic:"]],
- code = [[{"test", "^panic:"}]],
- },
- },
-}
-
-function Test.new(opts, pattern)
- if not pattern then
- pattern = opts
- opts = {}
- end
- return setmetatable({
- test = parser.util.patterns_to_test(pattern, opts.regex),
- }, { __index = Test })
-end
-
-function Test:reset() end
-
-function Test:ingest(line)
- if self.test(line) then
- return parser.STATUS.SUCCESS
- else
- return parser.STATUS.FAILURE
- end
-end
-
-return Test
diff --git a/lua/overseer/parser/util.lua b/lua/overseer/parser/util.lua
deleted file mode 100644
index 8824bafd..00000000
--- a/lua/overseer/parser/util.lua
+++ /dev/null
@@ -1,194 +0,0 @@
-local util = require("overseer.util")
----@diagnostic disable-next-line: deprecated
-local islist = vim.islist or vim.tbl_islist
-local M = {}
-
-M.append_item = function(append, line, ctx)
- if not append then
- return
- end
- local item = vim.tbl_deep_extend("keep", ctx.item, ctx.default_values or {})
- if type(append) == "function" then
- append(ctx.results, vim.deepcopy(item), { line = line })
- else
- table.insert(ctx.results, vim.deepcopy(item))
- end
-
- for k in pairs(ctx.item) do
- ctx.item[k] = nil
- end
-end
-
----@param data table|nil A parser or parser definition
-M.hydrate = function(data)
- vim.validate({
- data = { data, "t", true },
- })
- if not data then
- return nil
- end
- if data.ingest then
- return data
- else
- local constructor = require("overseer.parser")[data[1]]
- local args = util.tbl_slice(data, 2)
- return constructor(unpack(args))
- end
-end
-
----@param list table[]
-M.hydrate_list = function(list)
- vim.validate({
- list = { list, "t" },
- })
- local ret = {}
- for _, v in ipairs(list) do
- table.insert(ret, M.hydrate(v))
- end
- return ret
-end
-
----@param data table
----@return boolean
-M.is_parser = function(data)
- if type(data) ~= "table" then
- return false
- end
- return data.ingest or (islist(data) and type(data[1]) == "string")
-end
-
----@param list any
----@return boolean
-M.tbl_is_parser_list = function(list)
- if not islist(list) then
- return false
- end
- return util.list_all(list, M.is_parser)
-end
-
----@param pattern string|fun()
----@param regex boolean
----@return fun(line: string): boolean
-M.pattern_to_test = function(pattern, regex)
- if type(pattern) == "string" then
- if regex then
- return function(line)
- return vim.fn.match(line, pattern) >= 0
- end
- else
- return function(line)
- return line:match(pattern)
- end
- end
- else
- return pattern
- end
-end
-
----@param patterns string[]|fun()[]
----@param regex boolean
----@return fun(line: string): boolean
-M.patterns_to_test = function(patterns, regex)
- if type(patterns) ~= "table" then
- return M.pattern_to_test(patterns, regex)
- end
- local tests = {}
- for _, pat in ipairs(patterns) do
- table.insert(tests, M.pattern_to_test(pat, regex))
- end
-
- return function(line)
- for _, test in ipairs(tests) do
- if test(line) then
- return true
- end
- end
- return false
- end
-end
-
-local function default_postprocess_field(value, opts)
- if value:match("^%d+$") then
- return tonumber(value)
- elseif opts.field == "type" then
- return value:upper():match("^%w")
- else
- return value
- end
-end
-
----@param pattern string|fun(line: string)
----@param regex boolean
----@param fields string[]
----@return fun(line: string): nil|table
-M.pattern_to_extract = function(pattern, regex, fields)
- local match
- if type(pattern) == "string" then
- if regex then
- match = function(line)
- local result = vim.fn.matchlist(line, pattern)
- table.remove(result, 1)
- return result
- end
- else
- match = function(line)
- return { line:match(pattern) }
- end
- end
- else
- match = function(line)
- return { pattern(line) }
- end
- end
- return function(line)
- local result = match(line)
- if not result then
- return nil
- end
- local item
- for i, field in ipairs(fields) do
- if result[i] then
- if not item then
- item = {}
- end
- local key, postprocess
- if type(field) == "table" then
- key, postprocess = unpack(field)
- else
- key = field
- postprocess = default_postprocess_field
- end
- if key ~= "_" then
- item[key] = postprocess(result[i], { item = item, field = key })
- end
- end
- end
- return item
- end
-end
-
----@param patterns string[]|fun()[]
----@param regex boolean
----@param fields string[]
----@return fun(line: string): nil|table
-M.patterns_to_extract = function(patterns, regex, fields)
- if type(patterns) ~= "table" then
- return M.pattern_to_extract(patterns, regex, fields)
- end
-
- local extractors = {}
- for _, pat in ipairs(patterns) do
- table.insert(extractors, M.pattern_to_extract(pat, regex, fields))
- end
-
- return function(line)
- for _, ext in ipairs(extractors) do
- local item = ext(line)
- if item then
- return item
- end
- end
- end
-end
-
-return M
diff --git a/lua/overseer/render.lua b/lua/overseer/render.lua
new file mode 100644
index 00000000..a49b19a3
--- /dev/null
+++ b/lua/overseer/render.lua
@@ -0,0 +1,379 @@
+local util = require("overseer.util")
+
+local M = {}
+
+---@alias overseer.TextChunk {[1]: string, [2]: nil|string}
+
+---The default format for tasks in the task list
+---@param task overseer.Task
+---@return overseer.TextChunk[][]
+---@example
+--- require("overseer").setup({
+--- task_list = {
+--- render = function(task)
+--- return require("overseer.render").format_standard(task)
+--- end,
+--- },
+--- })
+M.format_standard = function(task)
+ local ret = {
+ M.status_and_name(task),
+ }
+ vim.list_extend(ret, M.source_lines(task))
+ table.insert(
+ ret,
+ M.join(M.duration(task), M.time_since_completed(task, { hl_group = "Comment" }))
+ )
+ vim.list_extend(ret, M.result_lines(task, { oneline = true }))
+ vim.list_extend(ret, M.output_lines(task, { num_lines = 1 }))
+ return M.remove_empty_lines(ret)
+end
+
+---A more compact format for tasks
+---@param task overseer.Task
+---@return overseer.TextChunk[][]
+---@example
+--- require("overseer").setup({
+--- task_list = {
+--- render = function(task)
+--- return require("overseer.render").format_compact(task)
+--- end,
+--- },
+--- })
+M.format_compact = function(task)
+ return {
+ M.status_and_name(task),
+ M.join(M.duration(task), M.time_since_completed(task, { hl_group = "Comment" })),
+ }
+end
+
+---A more verbose format for tasks
+---@param task overseer.Task
+---@return overseer.TextChunk[][]
+---@example
+--- require("overseer").setup({
+--- task_list = {
+--- render = function(task)
+--- return require("overseer.render").format_verbose(task)
+--- end,
+--- },
+--- })
+M.format_verbose = function(task)
+ local ret = {
+ M.status_and_name(task),
+ M.join(M.duration(task), M.time_since_completed(task, { hl_group = "Comment" })),
+ }
+ vim.list_extend(ret, M.result_lines(task, { oneline = true }))
+ vim.list_extend(ret, M.output_lines(task, { num_lines = 4 }))
+ return M.remove_empty_lines(ret)
+end
+
+---Text chunks that display the status of a task
+---@param task overseer.Task
+---@return overseer.TextChunk[]
+---@example
+--- require("overseer").setup({
+--- task_list = {
+--- render = function(task)
+--- local render = require("overseer.render")
+--- return {
+--- render.status(task),
+--- { { task.name, "OverseerTask" } },
+--- }
+--- end,
+--- },
+--- })
+M.status = function(task)
+ return { { task.status, "Overseer" .. task.status } }
+end
+
+---Text chunks that display the name of a task
+---@param task overseer.Task
+---@return overseer.TextChunk[]
+---@example
+--- require("overseer").setup({
+--- task_list = {
+--- render = function(task)
+--- local render = require("overseer.render")
+--- return {
+--- render.name(task),
+--- }
+--- end,
+--- },
+--- })
+M.name = function(task)
+ return { { task.name, "OverseerTask" } }
+end
+
+---Text chunks that display the status and name of a task
+---@param task overseer.Task
+---@return overseer.TextChunk[]
+---@example
+--- require("overseer").setup({
+--- task_list = {
+--- render = function(task)
+--- local render = require("overseer.render")
+--- return {
+--- render.status_and_name(task),
+--- }
+--- end,
+--- },
+--- })
+M.status_and_name = function(task)
+ return M.join(M.status(task), M.name(task), ": ")
+end
+
+---Text chunks that display the command that was run
+---@param task overseer.Task
+---@return overseer.TextChunk[]
+---@example
+--- require("overseer").setup({
+--- task_list = {
+--- render = function(task)
+--- local render = require("overseer.render")
+--- return {
+--- { { task.name, "OverseerTask" } },
+--- render.cmd(task),
+--- }
+--- end,
+--- },
+--- })
+M.cmd = function(task)
+ local cmd = task.cmd
+ if not cmd then
+ return {}
+ elseif type(cmd) == "string" then
+ return { { cmd } }
+ else
+ return { { table.concat(cmd, " ") } }
+ end
+end
+
+local function stringify_result(res)
+ if type(res) == "table" then
+ if vim.tbl_isempty(res) then
+ return "{}"
+ else
+ return string.format("{<%d items>}", vim.tbl_count(res))
+ end
+ else
+ return string.format("%s", res)
+ end
+end
+
+---Lines that display the result of a task
+---@param task overseer.Task
+---@param opts? {oneline?: boolean}
+---@return overseer.TextChunk[][]
+---@example
+--- require("overseer").setup({
+--- task_list = {
+--- render = function(task)
+--- local render = require("overseer.render")
+--- return vim.list_extend({
+--- { { task.name, "OverseerTask" } },
+--- }, render.result_lines(task, { oneline = false }))
+--- end,
+--- },
+--- })
+M.result_lines = function(task, opts)
+ ---@type {oneline: boolean}
+ opts = vim.tbl_extend("keep", opts or {}, { oneline = false })
+ if not task.result or vim.tbl_isempty(task.result) then
+ return {}
+ end
+ local ret = {}
+ if opts.oneline then
+ local pieces = {}
+ for k, v in pairs(task.result) do
+ table.insert(pieces, string.format("%s=%s", k, stringify_result(v)))
+ end
+ table.insert(ret, { { "Result: " }, { table.concat(pieces, ", ") } })
+ else
+ table.insert(ret, { { "Result: " } })
+ for k, v in pairs(task.result) do
+ table.insert(ret, { { string.format(" %s = %s", k, stringify_result(v)) } })
+ end
+ end
+ return ret
+end
+
+---Text chunks that display how long a task has been running / ran for
+---@param task overseer.Task
+---@param opts? {hl_group?: string}
+---@return overseer.TextChunk[]
+---@example
+--- require("overseer").setup({
+--- task_list = {
+--- render = function(task)
+--- local render = require("overseer.render")
+--- return {
+--- { { task.name, "OverseerTask" } },
+--- render.duration(task),
+--- }
+--- end,
+--- },
+--- })
+M.duration = function(task, opts)
+ opts = opts or {}
+ if not task.time_start then
+ return {}
+ end
+ local duration
+ if task.time_end then
+ duration = task.time_end - task.time_start
+ else
+ duration = os.time() - task.time_start
+ end
+ return { { util.format_duration(duration), opts.hl_group } }
+end
+
+---Text chunks that display the time since a task was completed
+---@param task overseer.Task
+---@param opts? {hl_group?: string}
+---@return overseer.TextChunk[]
+---@example
+--- require("overseer").setup({
+--- task_list = {
+--- render = function(task)
+--- local render = require("overseer.render")
+--- return {
+--- { { task.name, "OverseerTask" } },
+--- render.time_since_completed(task),
+--- }
+--- end,
+--- },
+--- })
+M.time_since_completed = function(task, opts)
+ opts = opts or {}
+ if not task.time_end then
+ return {}
+ end
+ return { { util.format_relative_timestamp(task.time_end), opts.hl_group } }
+end
+
+---Lines that display the last few lines of output from a task
+---@param task overseer.Task
+---@param opts? {num_lines?: integer, prefix?: string, prefix_hl_group?: string}
+---@return overseer.TextChunk[][]
+---@example
+--- require("overseer").setup({
+--- task_list = {
+--- render = function(task)
+--- local render = require("overseer.render")
+--- return vim.list_extend({
+--- { { task.name, "OverseerTask" } },
+--- }, render.output_lines(task, { num_lines = 3, prefix = "$ " }))
+--- end,
+--- },
+--- })
+M.output_lines = function(task, opts)
+ ---@type {num_lines: integer, prefix: string, prefix_hl_group: string}
+ opts = vim.tbl_extend(
+ "keep",
+ opts or {},
+ { num_lines = 1, prefix = "> ", prefix_hl_group = "Comment" }
+ )
+ local bufnr = task:get_bufnr()
+ if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then
+ return {}
+ end
+ local lines = util.get_last_output_lines(bufnr, opts.num_lines)
+ local ret = {}
+ for _, line in ipairs(lines) do
+ table.insert(ret, { { opts.prefix, opts.prefix_hl_group }, { line, "OverseerOutput" } })
+ end
+ return ret
+end
+
+---Lines that display the source of a wrapped vim.system or vim.fn.jobstart task
+---@param task overseer.Task
+---@param opts? {hl_group?: string}
+---@return overseer.TextChunk[][]
+---@example
+--- require("overseer").setup({
+--- task_list = {
+--- render = function(task)
+--- local render = require("overseer.render")
+--- return vim.list_extend({
+--- { { task.name, "OverseerTask" } },
+--- }, render.source_lines(task, { num_lines = 3, prefix = "$ " }))
+--- end,
+--- },
+--- })
+M.source_lines = function(task, opts)
+ ---@type {hl_group: string}
+ opts = vim.tbl_extend("keep", opts or {}, { hl_group = "Comment" })
+ local ret = {}
+ if task.source and task.source.module then
+ table.insert(ret, { { task.source.module, opts.hl_group } })
+ end
+ return ret
+end
+
+---Join two lists of text chunks together with a separator
+---@param a overseer.TextChunk[]
+---@param b overseer.TextChunk[]
+---@param sep? string|overseer.TextChunk
+---@return overseer.TextChunk[]
+---@example
+--- require("overseer").setup({
+--- task_list = {
+--- render = function(task)
+--- local render = require("overseer.render")
+--- return {
+--- render.join(render.status(task), render.name(task), ": "),
+--- }
+--- end,
+--- },
+--- })
+M.join = function(a, b, sep)
+ if not sep then
+ sep = " "
+ end
+ local ret = {}
+ for _, v in ipairs(a) do
+ table.insert(ret, v)
+ end
+ -- Only add the separator if there was anything in "a"
+ if #ret > 0 then
+ if type(sep) == "string" then
+ table.insert(ret, { sep })
+ else
+ table.insert(ret, sep)
+ end
+ end
+ for _, v in ipairs(b) do
+ table.insert(ret, v)
+ end
+ return ret
+end
+
+---Removes empty lines from a list of lines (each line is a list of text chunks)
+---@param lines overseer.TextChunk[][]
+---@return overseer.TextChunk[][]
+---@example
+--- require("overseer").setup({
+--- task_list = {
+--- render = function(task)
+--- local render = require("overseer.render")
+--- local ret = vim.list_extend({
+--- { { task.name, "OverseerTask" } },
+--- }, render.output_lines(task, { num_lines = 3, prefix = "$ " }))
+--- return render.remove_empty_lines(ret)
+--- end,
+--- },
+--- })
+M.remove_empty_lines = function(lines)
+ local i = 1
+ while i <= #lines do
+ if vim.tbl_isempty(lines[i]) then
+ table.remove(lines, i)
+ else
+ i = i + 1
+ end
+ end
+ return lines
+end
+
+return M
diff --git a/lua/overseer/strategy/_jobs.lua b/lua/overseer/strategy/_jobs.lua
deleted file mode 100644
index a42cdc2d..00000000
--- a/lua/overseer/strategy/_jobs.lua
+++ /dev/null
@@ -1,43 +0,0 @@
-local log = require("overseer.log")
-
-local M = {}
-
-local cleanup_autocmd
-local all_channels = {}
-
-M.register = function(job_id)
- if not cleanup_autocmd then
- -- Neovim will send a SIGHUP to PTY processes on exit. Unfortunately, some programs handle
- -- SIGHUP (for a legitimate purpose) and do not terminate, which leaves orphaned processes after
- -- Neovim exits. To avoid this, we need to explicitly call jobstop(), which will send a SIGHUP,
- -- wait (controlled by KILL_TIMEOUT_MS in process.c, 2000ms at the time of writing), then send a
- -- SIGTERM (possibly also a SIGKILL if that is insufficient).
- cleanup_autocmd = vim.api.nvim_create_autocmd("VimLeavePre", {
- desc = "Clean up running overseer tasks on exit",
- callback = function()
- local job_ids = vim.tbl_keys(all_channels)
- log:debug("VimLeavePre clean up terminal tasks %s", job_ids)
- for _, chan_id in ipairs(job_ids) do
- vim.fn.jobstop(chan_id)
- end
- local start_wait = vim.loop.hrtime()
- -- This makes sure Neovim doesn't exit until it has successfully killed all child processes.
- vim.fn.jobwait(job_ids)
- local elapsed = (vim.loop.hrtime() - start_wait) / 1e6
- if elapsed > 1000 then
- log:warn(
- "Killing running tasks took %dms. One or more processes likely did not terminate on SIGHUP. See https://github.com/stevearc/overseer.nvim/issues/46",
- elapsed
- )
- end
- end,
- })
- end
- all_channels[job_id] = true
-end
-
-M.unregister = function(job_id)
- all_channels[job_id] = nil
-end
-
-return M
diff --git a/lua/overseer/strategy/init.lua b/lua/overseer/strategy/init.lua
index 7381a932..69841781 100644
--- a/lua/overseer/strategy/init.lua
+++ b/lua/overseer/strategy/init.lua
@@ -1,58 +1,24 @@
-local config = require("overseer.config")
-local log = require("overseer.log")
local util = require("overseer.util")
local M = {}
---@class overseer.Strategy
---@field name string
----@field new function
---@field reset fun(self: overseer.Strategy)
---@field get_bufnr fun(): number|nil
---@field start fun(self: overseer.Strategy, task: overseer.Task)
---@field stop fun(self: overseer.Strategy)
---@field dispose fun(self: overseer.Strategy)
----@field render nil|fun(self: overseer.Strategy, lines: string[], highlights: table, detail: number)
-local NilStrategy = {}
-
----@return overseer.Strategy
-function NilStrategy.new()
- local strategy = {}
- setmetatable(strategy, { __index = NilStrategy })
- ---@cast strategy overseer.Strategy
- return strategy
-end
-
-function NilStrategy:reset() end
-
-function NilStrategy:get_bufnr() end
-
-function NilStrategy:start()
- error(string.format("Strategy '%s' not found", self.name))
-end
-
-function NilStrategy:stop() end
-
-function NilStrategy:dispose() end
-
----@param name_or_config? string|table
+---@param name_or_config overseer.Serialized
---@return overseer.Strategy
M.load = function(name_or_config)
- if not name_or_config then
- name_or_config = config.strategy
- end
local conf
local name
name, conf = util.split_config(name_or_config)
- local ok, strategy = pcall(require, string.format("overseer.strategy.%s", name))
- if ok then
- local instance = strategy.new(conf)
- instance.name = name
- return instance
- else
- log:error("No task strategy '%s'", name)
- return NilStrategy.new()
- end
+ local strategy = require(string.format("overseer.strategy.%s", name))
+ local instance = strategy.new(conf)
+ instance.name = name
+ return instance
end
return M
diff --git a/lua/overseer/strategy/jobstart.lua b/lua/overseer/strategy/jobstart.lua
index 29903b2d..3007710d 100644
--- a/lua/overseer/strategy/jobstart.lua
+++ b/lua/overseer/strategy/jobstart.lua
@@ -1,18 +1,58 @@
-local jobs = require("overseer.strategy._jobs")
local log = require("overseer.log")
+local overseer = require("overseer")
local util = require("overseer.util")
+local cleanup_autocmd
+local all_channels = {}
+local function register(job_id)
+ if not cleanup_autocmd then
+ -- Neovim will send a SIGHUP to PTY processes on exit. Unfortunately, some programs handle
+ -- SIGHUP (for a legitimate purpose) and do not terminate, which leaves orphaned processes after
+ -- Neovim exits. To avoid this, we need to explicitly call jobstop(), which will send a SIGHUP,
+ -- wait (controlled by KILL_TIMEOUT_MS in process.c, 2000ms at the time of writing), then send a
+ -- SIGTERM (possibly also a SIGKILL if that is insufficient).
+ cleanup_autocmd = vim.api.nvim_create_autocmd("VimLeavePre", {
+ desc = "Clean up running overseer tasks on exit",
+ callback = function()
+ local job_ids = vim.tbl_keys(all_channels)
+ log.debug("VimLeavePre clean up terminal tasks %s", job_ids)
+ for _, chan_id in ipairs(job_ids) do
+ vim.fn.jobstop(chan_id)
+ end
+ local start_wait = vim.uv.hrtime()
+ -- This makes sure Neovim doesn't exit until it has successfully killed all child processes.
+ vim.fn.jobwait(job_ids)
+ local elapsed = (vim.uv.hrtime() - start_wait) / 1e6
+ if elapsed > 1000 then
+ log.warn(
+ "Killing running tasks took %dms. One or more processes likely did not terminate on SIGHUP. See https://github.com/stevearc/overseer.nvim/issues/46",
+ elapsed
+ )
+ end
+ end,
+ })
+ end
+ all_channels[job_id] = true
+end
+
+local function unregister(job_id)
+ all_channels[job_id] = nil
+end
+
---@class overseer.JobstartStrategy : overseer.Strategy
---@field bufnr nil|integer
---@field job_id nil|integer
---@field term_id nil|integer
----@field opts table
+---@field opts overseer.JobstartStrategyOpts
local JobstartStrategy = {}
+---@class (exact) overseer.JobstartStrategyOpts
+---@field preserve_output? boolean If true, don't clear the buffer when tasks restart
+---@field use_terminal? boolean If false, use a normal non-terminal buffer to store the output. This may produce unwanted results if the task outputs terminal escape sequences.
+---@field wrap_opts? table Opts that were passed to jobstart(). We should wrap them
+
---Run tasks using jobstart()
----@param opts nil|table
---- preserve_output boolean If true, don't clear the buffer when tasks restart
---- use_terminal boolean If false, use a normal non-terminal buffer to store the output. This may produce unwanted results if the task outputs terminal escape sequences.
+---@param opts nil|overseer.JobstartStrategyOpts
---@return overseer.Strategy
function JobstartStrategy.new(opts)
opts = vim.tbl_extend("keep", opts or {}, {
@@ -23,6 +63,7 @@ function JobstartStrategy.new(opts)
bufnr = nil,
job_id = nil,
term_id = nil,
+ pending_output = {},
opts = opts,
}
setmetatable(strategy, { __index = JobstartStrategy })
@@ -36,7 +77,7 @@ function JobstartStrategy:reset()
self.bufnr = nil
self.term_id = nil
end
- if self.job_id then
+ if self.job_id and self.job_id > 0 then
vim.fn.jobstop(self.job_id)
self.job_id = nil
end
@@ -46,56 +87,123 @@ function JobstartStrategy:get_bufnr()
return self.bufnr
end
----@param task overseer.Task
-function JobstartStrategy:start(task)
- if not self.bufnr then
- self.bufnr = vim.api.nvim_create_buf(false, true)
- if self.opts.use_terminal then
- local mode = vim.api.nvim_get_mode().mode
- local term_id
- util.run_in_fullscreen_win(self.bufnr, function()
- term_id = vim.api.nvim_open_term(self.bufnr, {
- on_input = function(_, _, _, data)
- pcall(vim.api.nvim_chan_send, self.job_id, data)
- vim.defer_fn(function()
- util.terminal_tail_hack(self.bufnr)
- end, 10)
- end,
- })
- end)
- self.term_id = term_id
- -- Set the scrollback to max
- vim.bo[self.bufnr].scrollback = 100000
- util.hack_around_termopen_autocmd(mode)
+---@return boolean
+local function can_create_terminal()
+ -- Only allow creating a terminal in normal mode when we are not in a floating win.
+ -- Creating the terminal will exit visual/insert mode and can cause some dialogs to close.
+ return vim.api.nvim_get_mode().mode == "n" and not util.is_floating_win(0)
+end
+
+local pending_terminal_jobs = {}
+local function render_pending_terminals()
+ if not can_create_terminal() then
+ return
+ end
+ for _, strat in ipairs(pending_terminal_jobs) do
+ strat:_create_terminal()
+ end
+ pending_terminal_jobs = {}
+end
+
+local created_autocmds = false
+---@param strat overseer.JobstartStrategy
+local function queue_terminal_creation(strat)
+ table.insert(pending_terminal_jobs, strat)
+ if created_autocmds then
+ return
+ end
+ created_autocmds = true
+ vim.api.nvim_create_autocmd("ModeChanged", {
+ pattern = "*:n",
+ callback = render_pending_terminals,
+ })
+ vim.api.nvim_create_autocmd("WinEnter", {
+ callback = render_pending_terminals,
+ })
+end
+
+function JobstartStrategy:_create_terminal()
+ if not self.bufnr or not vim.api.nvim_buf_is_valid(self.bufnr) or self.term_id then
+ return
+ end
+ local term_id
+ util.run_in_fullscreen_win(self.bufnr, function()
+ term_id = vim.api.nvim_open_term(self.bufnr, {
+ on_input = function(_, _, _, data)
+ pcall(vim.api.nvim_chan_send, self.job_id, data)
+ vim.defer_fn(function()
+ util.terminal_tail_hack(self.bufnr)
+ end, 10)
+ end,
+ })
+ -- Set the scrollback to max
+ vim.bo[self.bufnr].scrollback = 100000
+ for _, data in ipairs(self.pending_output) do
+ pcall(vim.api.nvim_chan_send, term_id, table.concat(data, "\r\n"))
+ end
+ self.pending_output = {}
+ end)
+ self.term_id = term_id
+end
+
+function JobstartStrategy:_init_buffer()
+ local bufnr = vim.api.nvim_create_buf(false, true)
+ self.bufnr = bufnr
+ self.pending_output = {}
+ if self.opts.use_terminal then
+ if can_create_terminal() then
+ self:_create_terminal()
else
- vim.bo[self.bufnr].modifiable = false
- local function open_input()
- local prompt = vim.api.nvim_buf_get_lines(self.bufnr, -2, -1, true)[1]
- if prompt:match("^%s*$") then
- prompt = "Input: "
- end
- vim.ui.input({ prompt = prompt }, function(text)
- if text then
- pcall(vim.api.nvim_chan_send, self.job_id, text .. "\r")
- end
- end)
- end
- for _, lhs in ipairs({ "i", "I", "a", "A", "o", "O" }) do
- vim.keymap.set("n", lhs, open_input, { buffer = self.bufnr })
+ queue_terminal_creation(self)
+ end
+ else
+ vim.bo[self.bufnr].modifiable = false
+ local function open_input()
+ local prompt = vim.api.nvim_buf_get_lines(self.bufnr, -2, -1, true)[1]
+ if prompt:match("^%s*$") then
+ prompt = "Input: "
end
+ vim.ui.input({ prompt = prompt }, function(text)
+ if text then
+ pcall(vim.api.nvim_chan_send, self.job_id, text .. "\r")
+ end
+ end)
end
+ for _, lhs in ipairs({ "i", "I", "a", "A", "o", "O" }) do
+ vim.keymap.set("n", lhs, open_input, { buffer = self.bufnr })
+ end
+ end
+end
+
+---@param task overseer.Task
+function JobstartStrategy:start(task)
+ local wrap_term = self.opts.wrap_opts and self.opts.wrap_opts.term
+ local wrap = self.opts.wrap_opts or {}
+
+ if wrap_term then
+ -- If we are wrapping jobstart() and the user passed `term = true`, then they are intending to
+ -- use the current buffer as the output display.
+ self.bufnr = vim.api.nvim_get_current_buf()
+ end
+ if not self.bufnr then
+ self:_init_buffer()
end
- local job_id
local stdout_iter = util.get_stdout_line_iter()
local function on_stdout(data)
-- Update the buffer
- if self.opts.use_terminal then
- pcall(vim.api.nvim_chan_send, self.term_id, table.concat(data, "\r\n"))
- vim.defer_fn(function()
- util.terminal_tail_hack(self.bufnr)
- end, 10)
+ if wrap_term then
+ -- don't do anything
+ elseif self.opts.use_terminal then
+ if self.term_id then
+ pcall(vim.api.nvim_chan_send, self.term_id, table.concat(data, "\r\n"))
+ vim.defer_fn(function()
+ util.terminal_tail_hack(self.bufnr)
+ end, 10)
+ else
+ table.insert(self.pending_output, data)
+ end
else
-- Track which wins we will need to scroll
local trail_wins = {}
@@ -130,39 +238,65 @@ function JobstartStrategy:start(task)
task:dispatch("on_output_lines", lines)
end
end
- job_id = vim.fn.jobstart(task.cmd, {
+
+ local function coalesce(a, b)
+ if a == nil then
+ return b
+ else
+ return a
+ end
+ end
+
+ local opts = vim.tbl_extend("force", wrap, {
cwd = task.cwd,
env = task.env,
- pty = self.opts.use_terminal,
+ pty = coalesce(wrap.pty, self.opts.use_terminal),
-- Take 4 off the total width so it looks nice in the floating window
- width = vim.o.columns - 4,
- on_stdout = function(j, d)
+ width = coalesce(wrap.width, vim.o.columns - 4),
+ on_stdout = function(j, d, m)
+ if wrap.on_stdout then
+ wrap.on_stdout(j, d, m)
+ end
if self.job_id ~= j then
return
end
on_stdout(d)
end,
- on_stderr = function(j, d)
+ on_stderr = function(j, d, m)
+ if wrap.on_stderr then
+ wrap.on_stderr(j, d, m)
+ end
if self.job_id ~= j then
return
end
on_stdout(d)
end,
- on_exit = function(j, c)
- jobs.unregister(j)
+ on_exit = function(j, c, m)
+ if wrap.on_exit then
+ wrap.on_exit(j, c, m)
+ end
+ unregister(j)
if self.job_id ~= j then
return
end
- log:debug("Task %s exited with code %s", task.name, c)
+ log.debug("Task %s exited with code %s", task.name, c)
-- Feed one last line end to flush the output
on_stdout({ "" })
if self.opts.use_terminal then
- pcall(vim.api.nvim_chan_send, self.term_id, string.format("\r\n[Process exited %d]\r\n", c))
- -- HACK force terminal buffer to update
- -- see https://github.com/neovim/neovim/issues/23360
- vim.bo[self.bufnr].scrollback = vim.bo[self.bufnr].scrollback - 1
- vim.bo[self.bufnr].scrollback = vim.bo[self.bufnr].scrollback + 1
- util.terminal_tail_hack(self.bufnr)
+ if self.term_id then
+ pcall(
+ vim.api.nvim_chan_send,
+ self.term_id,
+ string.format("\r\n[Process exited %d]\r\n", c)
+ )
+ -- HACK force terminal buffer to update
+ -- see https://github.com/neovim/neovim/issues/23360
+ vim.bo[self.bufnr].scrollback = vim.bo[self.bufnr].scrollback - 1
+ vim.bo[self.bufnr].scrollback = vim.bo[self.bufnr].scrollback + 1
+ util.terminal_tail_hack(self.bufnr)
+ else
+ table.insert(self.pending_output, { "", string.format("[Process exited %d]", c), "" })
+ end
else
vim.bo[self.bufnr].modifiable = true
vim.api.nvim_buf_set_lines(
@@ -173,29 +307,32 @@ function JobstartStrategy:start(task)
{ string.format("[Process exited %d]", c), "" }
)
vim.bo[self.bufnr].modifiable = false
+ vim.bo[self.bufnr].modified = false
end
self.job_id = nil
-- If we're exiting vim, don't call the on_exit handler
-- We manually kill processes during VimLeavePre cleanup, and we don't want to trigger user
-- code because of that
if vim.v.exiting == vim.NIL then
+ ---@diagnostic disable-next-line: invisible
task:on_exit(c)
end
end,
})
- if job_id == 0 then
- error(string.format("Invalid arguments for task '%s'", task.name))
- elseif job_id == -1 then
- error(string.format("Command '%s' not executable", vim.inspect(task.cmd)))
+ self.job_id = overseer.builtin.jobstart(task.cmd, opts)
+
+ if self.job_id == 0 then
+ log.error("Invalid arguments for task '%s'", task.name)
+ elseif self.job_id == -1 then
+ log.error("Command '%s' not executable", task.cmd)
else
- jobs.register(job_id)
- self.job_id = job_id
+ register(self.job_id)
end
end
function JobstartStrategy:stop()
- if self.job_id then
+ if self.job_id and self.job_id > 0 then
vim.fn.jobstop(self.job_id)
self.job_id = nil
end
diff --git a/lua/overseer/strategy/orchestrator.lua b/lua/overseer/strategy/orchestrator.lua
index cb52a564..c936e0c6 100644
--- a/lua/overseer/strategy/orchestrator.lua
+++ b/lua/overseer/strategy/orchestrator.lua
@@ -7,8 +7,6 @@ local task_list = require("overseer.task_list")
local template = require("overseer.template")
local util = require("overseer.util")
local STATUS = constants.STATUS
----@diagnostic disable-next-line: deprecated
-local islist = vim.isarray or vim.tbl_islist
---Check if this is a reference to a defined task template
---@param task any
@@ -20,7 +18,7 @@ local function is_named_task(task)
end
assert(type(task) == "table", "Task must be a string or table")
- if islist(task) then
+ if vim.islist(task) then
-- If this is a list-like table, then this is not a named task.
-- It will be a list of named tasks or task definitions.
return false
@@ -72,19 +70,15 @@ local OrchestratorStrategy = {}
--- },
--- })
function OrchestratorStrategy.new(opts)
- vim.validate({
- opts = { opts, "t" },
- })
- vim.validate({
- tasks = { opts.tasks, "t" },
- })
+ vim.validate("opts", opts, "table")
+ vim.validate("opts.tasks", opts.tasks, "table")
-- Each entry in tasks can be either a task definition, OR a list of task definitions.
-- Convert it to each entry being a list of task definitions.
local task_defns = {}
for i, v in ipairs(opts.tasks) do
if is_named_task(v) then
task_defns[i] = { v }
- elseif islist(v) then
+ elseif vim.islist(v) then
task_defns[i] = v
else
task_defns[i] = { v }
@@ -108,9 +102,6 @@ function OrchestratorStrategy:render_buf()
local ns = vim.api.nvim_create_namespace("overseer")
vim.api.nvim_buf_clear_namespace(self.bufnr, ns, 0, -1)
- local lines = {}
- local highlights = {}
-
local columns = {}
local col_widths = {}
local max_row = 0
@@ -128,34 +119,25 @@ function OrchestratorStrategy:render_buf()
max_row = math.max(max_row, #columns[i])
end
+ local lines = {}
for i = 1, max_row do
local line = {}
- local col_start = 0
for j, column in ipairs(columns) do
local task = column[i]
+ if j > 1 then
+ table.insert(line, { " -> " })
+ end
if task then
- table.insert(
- line,
- util.ljust(string.format("%s %s", task.status, task.name), col_widths[j])
- )
- local col_end = col_start + task.status:len()
- table.insert(
- highlights,
- { string.format("Overseer%s", task.status), #lines + 1, col_start, col_end }
- )
+ table.insert(line, { task.status, string.format("Overseer%s", task.status) })
+ table.insert(line, { util.ljust(" " .. task.name, col_widths[j] - #task.status) })
else
- table.insert(line, string.rep(" ", col_widths[j]))
+ table.insert(line, { string.rep(" ", col_widths[j]) })
end
- col_start = col_start + line[#line]:len() + 4
end
- table.insert(lines, table.concat(line, " -> "))
+ table.insert(lines, line)
end
- vim.bo[self.bufnr].modifiable = true
- vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, true, lines)
- vim.bo[self.bufnr].modifiable = false
- vim.bo[self.bufnr].modified = false
- util.add_highlights(self.bufnr, ns, highlights)
+ util.render_buf_chunks(self.bufnr, ns, lines)
end
function OrchestratorStrategy:reset()
@@ -199,6 +181,7 @@ function OrchestratorStrategy:start_next()
break
elseif status == STATUS.FAILURE or status == STATUS.CANCELED then
if self.task and self.task:is_running() then
+ ---@diagnostic disable-next-line: invisible
self.task:finalize(status)
end
break
@@ -206,6 +189,7 @@ function OrchestratorStrategy:start_next()
all_success = i == #self.tasks
end
if all_success then
+ ---@diagnostic disable-next-line: invisible
self.task:finalize(STATUS.SUCCESS)
end
end
@@ -225,9 +209,6 @@ function OrchestratorStrategy:build_task(defn, i, j)
---@param task overseer.Task
local function finalize_subtask(task)
task:add_component("orchestrator.on_status_broadcast")
- -- Don't include child tasks when saving to bundle. We will re-create them when the
- -- orchestration task is loaded.
- task:set_include_in_bundle(false)
self.tasks[i][j] = task.id
if self:section_complete(1) then
self:start_next()
@@ -235,7 +216,8 @@ function OrchestratorStrategy:build_task(defn, i, j)
end
if type(defn) == "table" and defn[1] == nil then
- defn = vim.tbl_extend("error", { parent_id = self.task.id }, defn)
+ defn = vim.tbl_extend("error", { parent_id = self.task.id, ephemeral = true }, defn)
+ ---@cast defn overseer.TaskDefinition
local task = require("overseer").new_task(defn)
finalize_subtask(task)
return
@@ -245,7 +227,8 @@ function OrchestratorStrategy:build_task(defn, i, j)
params = params or {}
template.get_by_name(name, search, function(tmpl)
if not tmpl then
- log:error("Orchestrator could not find task '%s'", name)
+ log.error("Orchestrator could not find task '%s'", name)
+ ---@diagnostic disable-next-line: invisible
self.task:finalize(STATUS.FAILURE)
return
end
@@ -258,7 +241,8 @@ function OrchestratorStrategy:build_task(defn, i, j)
build_opts,
vim.schedule_wrap(function(task_defn)
if not task_defn then
- log:warn("Canceled building task '%s'", name)
+ log.warn("Canceled building task '%s'", name)
+ ---@diagnostic disable-next-line: invisible
self.task:finalize(STATUS.FAILURE)
return
end
diff --git a/lua/overseer/strategy/system.lua b/lua/overseer/strategy/system.lua
new file mode 100644
index 00000000..b3b5ba9d
--- /dev/null
+++ b/lua/overseer/strategy/system.lua
@@ -0,0 +1,274 @@
+local log = require("overseer.log")
+local overseer = require("overseer")
+local util = require("overseer.util")
+
+---@param proc vim.SystemObj
+local function graceful_kill(proc)
+ proc:kill("SIGTERM")
+ vim.defer_fn(function()
+ if not proc:is_closing() then
+ proc:kill("SIGKILL")
+ end
+ end, 5000)
+end
+
+local cleanup_autocmd
+local all_procs = {}
+---@param proc vim.SystemObj
+local function register(proc)
+ if not cleanup_autocmd then
+ cleanup_autocmd = vim.api.nvim_create_autocmd("VimLeavePre", {
+ desc = "Clean up running overseer tasks on exit",
+ callback = function()
+ if #all_procs == 0 then
+ return
+ end
+ log.debug("VimLeavePre clean up %d vim.system processes", #all_procs)
+ for _, p in ipairs(all_procs) do
+ graceful_kill(p)
+ end
+ local start_wait = vim.uv.now()
+ if vim.wait(5001, function()
+ return #all_procs == 0
+ end) then
+ return
+ end
+
+ local elapsed = (vim.uv.now() - start_wait)
+ if elapsed > 1000 then
+ log.warn(
+ "Killing running vim.system tasks took %dms. One or more processes likely did not terminate on SIGHUP. See https://github.com/stevearc/overseer.nvim/issues/46",
+ elapsed
+ )
+ end
+ end,
+ })
+ end
+ table.insert(all_procs, proc)
+end
+
+---@param proc vim.SystemObj
+local function unregister(proc)
+ for i, p in ipairs(all_procs) do
+ if p == proc then
+ table.remove(all_procs, i)
+ return
+ end
+ end
+end
+
+---@class overseer.SystemStrategy : overseer.Strategy
+---@field bufnr nil|integer
+---@field handle nil|vim.SystemObj
+---@field opts overseer.SystemStrategyOpts
+local SystemStrategy = {}
+
+---@class (exact) overseer.SystemStrategyOpts
+---@field wrap_opts? vim.SystemOpts Opts that were passed to vim.system(). We should wrap them
+---@field wrap_exit? fun(out: vim.SystemCompleted)
+
+---@param opts nil|overseer.SystemStrategyOpts
+---@return overseer.Strategy
+function SystemStrategy.new(opts)
+ local strategy = {
+ bufnr = nil,
+ job_id = nil,
+ term_id = nil,
+ opts = opts or {},
+ }
+ setmetatable(strategy, { __index = SystemStrategy })
+ ---@type overseer.SystemStrategy
+ return strategy
+end
+
+function SystemStrategy:reset()
+ if self.bufnr then
+ util.soft_delete_buf(self.bufnr)
+ self.bufnr = nil
+ end
+ if self.handle then
+ graceful_kill(self.handle)
+ self.handle = nil
+ end
+end
+
+function SystemStrategy:get_bufnr()
+ return self.bufnr
+end
+
+---@param task overseer.Task
+function SystemStrategy:start(task)
+ local wrap = self.opts.wrap_opts or {}
+
+ if not self.bufnr then
+ self.bufnr = vim.api.nvim_create_buf(false, true)
+ vim.bo[self.bufnr].modifiable = false
+ end
+
+ local stdout_iter = util.get_stdout_line_iter()
+
+ ---@param data string
+ local on_output = vim.schedule_wrap(function(data)
+ -- Update the buffer
+ -- Track which wins we will need to scroll
+ local trail_wins = {}
+ local line_count = vim.api.nvim_buf_line_count(self.bufnr)
+ for _, winid in ipairs(vim.api.nvim_list_wins()) do
+ if vim.api.nvim_win_is_valid(winid) and vim.api.nvim_win_get_buf(winid) == self.bufnr then
+ if vim.api.nvim_win_get_cursor(winid)[1] == line_count then
+ table.insert(trail_wins, winid)
+ end
+ end
+ end
+ local end_line = vim.api.nvim_buf_get_lines(self.bufnr, -2, -1, true)[1] or ""
+ if not wrap.text then
+ data = data:gsub("\r", "")
+ end
+ local raw_data = vim.split(data, "\n")
+ local lines = raw_data
+ lines[1] = end_line .. lines[1]
+ vim.bo[self.bufnr].modifiable = true
+ vim.api.nvim_buf_set_lines(self.bufnr, -2, -1, true, lines)
+ vim.bo[self.bufnr].modifiable = false
+ vim.bo[self.bufnr].modified = false
+
+ -- Scroll to end of updated windows so we can tail output
+ local lnum = line_count + #lines - 1
+ local col = vim.api.nvim_strwidth(lines[#lines])
+ for _, winid in ipairs(trail_wins) do
+ vim.api.nvim_win_set_cursor(winid, { lnum, col })
+ end
+
+ -- Send output to task
+ task:dispatch("on_output", raw_data)
+ local iter_lines = stdout_iter(raw_data)
+ if not vim.tbl_isempty(iter_lines) then
+ task:dispatch("on_output_lines", iter_lines)
+ end
+ end)
+
+ local handle
+ local outputs = {}
+ if wrap.stdout == nil or wrap.stdout == true then
+ outputs.stdout = {}
+ end
+ if wrap.stderr == nil or wrap.stderr == true then
+ outputs.stderr = {}
+ end
+
+ local function output_fn(channel)
+ return function(err, data)
+ if type(wrap[channel]) == "function" then
+ local ok, cb_err = pcall(wrap[channel], err, data)
+ if not ok then
+ vim.schedule(function()
+ log.error("Error in %s %s callback: %s", task.name, channel, cb_err)
+ end)
+ end
+ end
+ if outputs[channel] and data then
+ table.insert(outputs[channel], data)
+ end
+ if err then
+ vim.schedule(function()
+ log.error("Error in %s %s callback: %s", task.name, channel, err)
+ end)
+ end
+ if self.handle == handle and data then
+ on_output(data)
+ end
+ end
+ end
+
+ local opts = vim.tbl_extend("force", wrap, {
+ cwd = task.cwd,
+ env = task.env,
+ stdout = output_fn("stdout"),
+ stderr = output_fn("stderr"),
+ })
+ ---@param out vim.SystemCompleted
+ local function on_exit(out)
+ -- If we patched the opts to include a stdout/stderr callback function where there was none
+ -- before, we need to manually set those values on the SystemCompleted object
+ if not out.stdout and outputs.stdout then
+ out.stdout = table.concat(outputs.stdout)
+ end
+ if wrap.stderr and outputs.stderr then
+ out.stderr = table.concat(outputs.stderr)
+ end
+
+ if self.opts.wrap_exit then
+ self.opts.wrap_exit(out)
+ end
+ unregister(handle)
+ if self.handle ~= handle then
+ return
+ end
+
+ -- The rest of this needs to not happen in a fast event
+ vim.schedule(function()
+ log.debug("Task %s exited with code %s", task.name, out.code)
+ -- Feed one last line end to flush the output
+ on_output("\n")
+ vim.bo[self.bufnr].modifiable = true
+ vim.api.nvim_buf_set_lines(
+ self.bufnr,
+ -1,
+ -1,
+ true,
+ { string.format("[Process exited %d]", out.code), "" }
+ )
+ vim.bo[self.bufnr].modifiable = false
+ vim.bo[self.bufnr].modified = false
+ self.handle = nil
+ -- If we're exiting vim, don't call the on_exit handler
+ -- We manually kill processes during VimLeavePre cleanup, and we don't want to trigger user
+ -- code because of that
+ if vim.v.exiting == vim.NIL then
+ ---@diagnostic disable-next-line: invisible
+ task:on_exit(out.code)
+ end
+ end)
+ end
+
+ local cmd = task.cmd
+ ---@cast cmd string[]
+ handle = overseer.builtin.system(cmd, opts, on_exit)
+
+ local raw_wait = handle.wait
+ -- NOTE: In practice the .stdout/.stderr patching done in on_exit is all we need, because the same
+ -- object is returned from wait(). However, we're patching the wait function just in case the
+ -- internal implementation changes at some point.
+ handle.wait = function(p, timeout)
+ local out = raw_wait(p, timeout)
+ -- If we patched the opts to include a stdout/stderr callback function where there was none
+ -- before, we need to manually set those values on the SystemCompleted object
+ if not out.stdout and outputs.stdout then
+ out.stdout = table.concat(outputs.stdout)
+ end
+ if wrap.stderr and outputs.stderr then
+ out.stderr = table.concat(outputs.stderr)
+ end
+ return out
+ end
+
+ self.handle = handle
+
+ if not wrap.detach then
+ register(self.handle)
+ end
+end
+
+function SystemStrategy:stop()
+ if self.handle then
+ graceful_kill(self.handle)
+ self.handle = nil
+ end
+end
+
+function SystemStrategy:dispose()
+ self:stop()
+ util.soft_delete_buf(self.bufnr)
+end
+
+return SystemStrategy
diff --git a/lua/overseer/strategy/terminal.lua b/lua/overseer/strategy/terminal.lua
deleted file mode 100644
index 8a23494e..00000000
--- a/lua/overseer/strategy/terminal.lua
+++ /dev/null
@@ -1,111 +0,0 @@
-local jobs = require("overseer.strategy._jobs")
-local log = require("overseer.log")
-local util = require("overseer.util")
-
----@class overseer.TerminalStrategy : overseer.Strategy
----@field bufnr nil|integer
----@field chan_id nil|integer
-local TerminalStrategy = {}
-
----Run tasks using termopen()
----@return overseer.Strategy
-function TerminalStrategy.new()
- local strategy = {
- bufnr = nil,
- chan_id = nil,
- }
- setmetatable(strategy, { __index = TerminalStrategy })
- ---@type overseer.TerminalStrategy
- return strategy
-end
-
-function TerminalStrategy:reset()
- util.soft_delete_buf(self.bufnr)
- self.bufnr = nil
- if self.chan_id then
- vim.fn.jobstop(self.chan_id)
- self.chan_id = nil
- end
-end
-
-function TerminalStrategy:get_bufnr()
- return self.bufnr
-end
-
----@param task overseer.Task
-function TerminalStrategy:start(task)
- self.bufnr = vim.api.nvim_create_buf(false, true)
- local chan_id
- local mode = vim.api.nvim_get_mode().mode
- local stdout_iter = util.get_stdout_line_iter()
-
- local function on_stdout(data)
- task:dispatch("on_output", data)
- local lines = stdout_iter(data)
- if not vim.tbl_isempty(lines) then
- task:dispatch("on_output_lines", lines)
- end
- vim.defer_fn(function()
- util.terminal_tail_hack(self.bufnr)
- end, 10)
- end
- util.run_in_fullscreen_win(self.bufnr, function()
- chan_id = vim.fn.termopen(task.cmd, {
- cwd = task.cwd,
- env = task.env,
- on_stdout = function(j, d)
- if self.chan_id ~= j then
- return
- end
- on_stdout(d)
- end,
- on_exit = function(j, c)
- jobs.unregister(j)
- if self.chan_id ~= j then
- return
- end
- log:debug("Task %s exited with code %s", task.name, c)
- -- Feed one last line end to flush the output
- on_stdout({ "" })
- -- HACK force terminal buffer to update
- -- see https://github.com/neovim/neovim/issues/23360
- vim.bo[self.bufnr].scrollback = vim.bo[self.bufnr].scrollback - 1
- vim.bo[self.bufnr].scrollback = vim.bo[self.bufnr].scrollback + 1
- self.chan_id = nil
- -- If we're exiting vim, don't call the on_exit handler
- -- We manually kill processes during VimLeavePre cleanup, and we don't want to trigger user
- -- code because of that
- if vim.v.exiting == vim.NIL then
- task:on_exit(c)
- end
- end,
- })
- end)
-
- -- Set the scrollback to max
- vim.bo[self.bufnr].scrollback = 100000
- util.hack_around_termopen_autocmd(mode)
-
- if chan_id == 0 then
- error(string.format("Invalid arguments for task '%s'", task.name))
- elseif chan_id == -1 then
- error(string.format("Command '%s' not executable", vim.inspect(task.cmd)))
- else
- jobs.register(chan_id)
- self.chan_id = chan_id
- end
-end
-
-function TerminalStrategy:stop()
- if self.chan_id then
- vim.fn.jobstop(self.chan_id)
- self.chan_id = nil
- end
-end
-
-function TerminalStrategy:dispose()
- self:stop()
- util.soft_delete_buf(self.bufnr)
-end
-
-return TerminalStrategy
diff --git a/lua/overseer/strategy/test.lua b/lua/overseer/strategy/test.lua
index 808deaec..ac153be2 100644
--- a/lua/overseer/strategy/test.lua
+++ b/lua/overseer/strategy/test.lua
@@ -43,6 +43,7 @@ function TestStrategy:send_exit(code)
-- Feed one last line end to flush the output
self.task:dispatch("on_output", "\n")
self.task:dispatch("on_output_lines", { "" })
+ ---@diagnostic disable-next-line: invisible
self.task:on_exit(code or 0)
end
diff --git a/lua/overseer/strategy/toggleterm.lua b/lua/overseer/strategy/toggleterm.lua
deleted file mode 100644
index 081af043..00000000
--- a/lua/overseer/strategy/toggleterm.lua
+++ /dev/null
@@ -1,159 +0,0 @@
-local jobs = require("overseer.strategy._jobs")
-local shell = require("overseer.shell")
-local util = require("overseer.util")
-
-local terminal = require("toggleterm.terminal")
-
----@class overseer.ToggleTermStrategy : overseer.Strategy
----@field private opts overseeer.ToggleTermStrategyOpts
----@field private term? Terminal
-local ToggleTermStrategy = {}
-
----@class overseeer.ToggleTermStrategyOpts
----@field use_shell? boolean load user shell before running task
----@field size? number the size of the split if direction is vertical or horizontal
----@field direction? "vertical"|"horizontal"|"tab"|"float"
----@field highlights? table map to a highlight group name and a table of it's values
----@field auto_scroll? boolean automatically scroll to the bottom on task output
----@field close_on_exit? boolean close the terminal and delete terminal buffer (if open) after task exits
----@field quit_on_exit? "never"|"always"|"success" close the terminal window (if open) after task exits
----@field open_on_start? boolean toggle open the terminal automatically when task starts
----@field hidden? boolean cannot be toggled with normal ToggleTerm commands
----@field on_create? fun(term: table) function to execute on terminal creation
-
----Run tasks using the toggleterm plugin
----@param opts? overseeer.ToggleTermStrategyOpts
----@return overseer.Strategy
-function ToggleTermStrategy.new(opts)
- opts = vim.tbl_extend("keep", opts or {}, {
- use_shell = false,
- size = nil,
- direction = nil,
- highlights = nil,
- auto_scroll = nil,
- close_on_exit = false,
- quit_on_exit = "never",
- open_on_start = true,
- hidden = false,
- on_create = nil,
- })
- local strategy = {
- opts = opts,
- term = nil,
- }
- setmetatable(strategy, { __index = ToggleTermStrategy })
- ---@type overseer.ToggleTermStrategy
- return strategy
-end
-
-function ToggleTermStrategy:reset()
- if self.term then
- self.term:shutdown()
- self.term = nil
- end
-end
-
-function ToggleTermStrategy:get_bufnr()
- return self.term and self.term.bufnr
-end
-
----@param task overseer.Task
-function ToggleTermStrategy:start(task)
- local mode = vim.api.nvim_get_mode().mode
- local stdout_iter = util.get_stdout_line_iter()
-
- local function on_stdout(data)
- task:dispatch("on_output", data)
- local lines = stdout_iter(data)
- if not vim.tbl_isempty(lines) then
- task:dispatch("on_output_lines", lines)
- end
- end
-
- local cmd = task.cmd
- if type(cmd) == "table" then
- cmd = shell.escape_cmd(cmd, "strong")
- end
-
- local passed_cmd
- if not self.opts.use_shell then
- passed_cmd = cmd
- end
-
- self.term = terminal.Terminal:new({
- cmd = passed_cmd,
- env = task.env,
- highlights = self.opts.highlights,
- dir = task.cwd,
- direction = self.opts.direction,
- auto_scroll = self.opts.auto_scroll,
- close_on_exit = self.opts.close_on_exit,
- hidden = self.opts.hidden,
- on_create = function(t)
- local job_id = t.job_id
- jobs.register(job_id)
-
- if self.opts.on_create then
- self.opts.on_create(t)
- end
-
- if self.opts.use_shell then
- t:send(cmd)
- t:send("exit " .. (vim.o.shell:find("fish") and "$status" or "$?"))
- end
- end,
- on_stdout = function(t, job_id, d)
- if t ~= self.term then
- return
- end
- on_stdout(d)
- end,
- on_exit = function(t, j, c)
- jobs.unregister(j)
- if t ~= self.term then
- return
- end
- -- Feed one last line end to flush the output
- on_stdout({ "" })
- if vim.v.exiting == vim.NIL then
- task:on_exit(c)
- end
-
- local close = self.opts.quit_on_exit == "always"
- or (self.opts.quit_on_exit == "success" and c == 0)
- if close then
- t:close()
- end
- end,
- })
-
- if self.opts.open_on_start then
- self.term:toggle(self.opts.size)
- else
- self.term:spawn()
- end
-
- util.hack_around_termopen_autocmd(mode)
-end
-
-function ToggleTermStrategy:stop()
- if self.term and self.term.job_id then
- vim.fn.jobstop(self.term.job_id)
- end
-end
-
-function ToggleTermStrategy:dispose()
- if self.term then
- self.term:shutdown()
- self.term = nil
- end
-end
-
----@param direction "float"|"tab"|"vertical"|"horizontal"
-function ToggleTermStrategy:open_terminal(direction)
- if self.term then
- self.term:open(self.opts.size, direction)
- end
-end
-
-return ToggleTermStrategy
diff --git a/lua/overseer/task.lua b/lua/overseer/task.lua
index 02691018..5aede0ea 100644
--- a/lua/overseer/task.lua
+++ b/lua/overseer/task.lua
@@ -1,7 +1,6 @@
local component = require("overseer.component")
local config = require("overseer.config")
local constants = require("overseer.constants")
-local form_utils = require("overseer.form.utils")
local layout = require("overseer.layout")
local log = require("overseer.log")
local shell = require("overseer.shell")
@@ -12,36 +11,33 @@ local util = require("overseer.util")
local STATUS = constants.STATUS
---@class overseer.Task
----@field id number
----@field result? table
----@field metadata table
----@field default_component_params table
----@field status overseer.Status
----@field cmd string|string[]
----@field cwd string
----@field env? table
----@field strategy_defn string|table
----@field strategy overseer.Strategy
----@field name string
----@field exit_code? number
----@field components overseer.Component[]
+---@opaque
+---@field id integer Unique ID for this task
+---@field result? table For successful tasks, arbitrary key-value mapping of data produced by components
+---@field metadata table Arbitrary key-value mapping passed by the user during construction
+---@field private default_component_params table
+---@field status overseer.Status Current task status
+---@field cmd string|string[] Command to run. If it's a string it is run in the shell
+---@field cwd string Working directory the task is run in
+---@field env? table Additional environment variables for the task
+---@field private strategy_defn string|table
+---@field private strategy overseer.Strategy
+---@field name string Name of the task
+---@field ephemeral boolean Indicates that this task was generated indirectly (e.g. with run_after)
+---@field source? overseer.Caller If this task was created by wrapping jobstart/vim.system, this contains information about the callsite
+---@field exit_code? integer Exit code of the task process
+---@field private components overseer.Component[]
---@field parent_id? integer ID of parent task. Used only to visually group tasks in the task list
+---@field time_start? integer Timestamp when the task was started (os.time())
+---@field time_end? integer Timestamp when the task ended (os.time())
+---@field private from_template? overseer.TemplateSource
---@field private prev_bufnr? integer
----@field private _subscribers table
+---@field private _subscribers table
local Task = {}
-local next_id = 1
+---@alias overseer.TaskEventHandler fun(task: overseer.Task, ...: any): nil|boolean
-Task.ordered_params = { "cmd", "cwd" }
----@type overseer.Params
-Task.params = {
- -- It's kind of a hack to specify a delimiter without type = 'list'. This is
- -- so the task editor displays nicely if the value is a list OR a string
- cmd = { delimiter = " " },
- cwd = {
- optional = true,
- },
-}
+local next_id = 1
---@class (exact) overseer.TaskDefinition
---@field cmd string|string[] Command to run. If it's a string it is run in the shell; a table is run directly
@@ -53,25 +49,32 @@ Task.params = {
---@field metadata? table Arbitrary metadata for your own use
---@field default_component_params? table Default values for component params
---@field components? overseer.Serialized[] List of components to attach. Defaults to `{"default"}`
+---@field ephemeral? boolean Indicates that this task was generated by another task (e.g. with run_after)
+---@field private from_template? overseer.TemplateSource
+
+---@class (exact) overseer.TemplateSource
+---@field name string Name of the template
+---@field env? table
+---@field params table
+---@field search overseer.SearchParams
----Create an uninitialized Task with no ID that will not be run
----This is used by the Task previewer (loading task bundles) so that we can use
----the Task rendering logic, but don't end up actually creating & registering a
----Task.
+---Create a new Task
---@param opts overseer.TaskDefinition
---@return overseer.Task
-function Task.new_uninitialized(opts)
+function Task.new(opts)
opts = opts or {}
- vim.validate({
- -- cmd can be table or string
- args = { opts.args, "t", true },
- cwd = { opts.cwd, "s", true },
- env = { opts.env, "t", true },
- name = { opts.name, "s", true },
- components = { opts.components, "t", true },
- metadata = { opts.metadata, "t", true },
- default_component_params = { opts.default_component_params, "t", true },
- })
+ log.trace("New task: %s", opts)
+ -- cmd can be table or string
+ vim.validate("cmd", opts.name, function(v)
+ return type(v) == "string" or type(v) == "table"
+ end, true)
+ vim.validate("args", opts.args, "table", true)
+ vim.validate("cwd", opts.cwd, "string", true)
+ vim.validate("env", opts.env, "table", true)
+ vim.validate("name", opts.name, "string", true)
+ vim.validate("components", opts.components, "table", true)
+ vim.validate("metadata", opts.metadata, "table", true)
+ vim.validate("default_component_params", opts.default_component_params, "table", true)
if opts.env and vim.tbl_isempty(opts.env) then
-- For some reason termopen() doesn't like an empty env table
opts.env = nil
@@ -102,16 +105,21 @@ function Task.new_uninitialized(opts)
name = name:gsub("\n", " ")
if not opts.strategy then
- opts.strategy = config.strategy
+ opts.strategy = {
+ "jobstart",
+ use_terminal = config.output.use_terminal,
+ preserve_output = config.output.preserve_output,
+ }
end
-- Build the instance data for the task
local task = {
+ id = next_id,
result = nil,
metadata = opts.metadata or {},
default_component_params = opts.default_component_params or {},
_references = 0,
- _include_in_bundle = true,
+ ephemeral = opts.ephemeral == true,
_subscribers = {},
status = STATUS.PENDING,
cmd = opts.cmd,
@@ -124,125 +132,27 @@ function Task.new_uninitialized(opts)
prev_bufnr = nil,
components = {},
-- for internal use
+ ---@diagnostic disable-next-line: invisible
+ from_template = opts.from_template,
---@diagnostic disable-next-line: undefined-field
parent_id = opts.parent_id,
+ ---@diagnostic disable-next-line: undefined-field
+ source = opts.source,
}
+ next_id = next_id + 1
setmetatable(task, { __index = Task })
task:add_components(opts.components)
---@cast task overseer.Task
- return task
-end
-
----@param opts overseer.TaskDefinition
----@return overseer.Task
-function Task.new(opts)
- log:trace("New task: %s", opts)
- local task = Task.new_uninitialized(opts)
- task.id = next_id
- next_id = next_id + 1
task:dispatch("on_init")
local bufnr = task:get_bufnr()
if bufnr then
vim.b[bufnr].overseer_task = task.id
end
+ task:subscribe("on_status", task_list.on_task_updated)
return task
end
-local function stringify_result(res)
- if type(res) == "table" then
- if vim.tbl_isempty(res) then
- return "{}"
- else
- return string.format("{<%d items>}", vim.tbl_count(res))
- end
- else
- return string.format("%s", res)
- end
-end
-
-function Task:render(lines, highlights, detail)
- vim.validate({
- lines = { lines, "t" },
- detail = { detail, "n" },
- })
- table.insert(lines, string.format("%s: %s", self.status, self.name))
- table.insert(highlights, { "Overseer" .. self.status, #lines, 0, string.len(self.status) })
- table.insert(highlights, { "OverseerTask", #lines, string.len(self.status) + 2, -1 })
-
- if self.strategy.render then
- self.strategy:render(lines, highlights, detail)
- end
-
- if detail > 1 and self.cmd then
- local cmd = self.cmd
- local cmd_str
- if type(cmd) == "string" then
- cmd_str = cmd
- else
- cmd_str = table.concat(cmd, " ")
- end
- table.insert(lines, cmd_str)
- end
-
- -- Render components
- if detail >= 3 then
- for _, comp in ipairs(self.components) do
- if comp.desc then
- table.insert(lines, string.format("%s (%s)", comp.name, comp.desc))
- table.insert(highlights, { "OverseerComponent", #lines, 0, string.len(comp.name) })
- table.insert(highlights, { "Comment", #lines, string.len(comp.name) + 1, -1 })
- else
- table.insert(lines, comp.name)
- end
-
- local comp_def = assert(component.get(comp.name))
- for k, v in pairs(comp.params) do
- if k ~= 1 then
- table.insert(lines, form_utils.render_field(comp_def.params[k], " ", k, v))
- end
- end
-
- if comp.render then
- comp:render(self, lines, highlights, detail)
- end
- end
- else
- for _, comp in ipairs(self.components) do
- if comp.render then
- comp:render(self, lines, highlights, detail)
- end
- end
- end
-
- -- Render the result
- if self.result and not vim.tbl_isempty(self.result) then
- if detail == 1 then
- local pieces = {}
- for k, v in pairs(self.result) do
- table.insert(pieces, string.format("%s=%s", k, stringify_result(v)))
- end
- table.insert(lines, "Result: " .. table.concat(pieces, ", "))
- else
- table.insert(lines, "Result:")
- for k, v in pairs(self.result) do
- table.insert(lines, string.format(" %s = %s", k, stringify_result(v)))
- end
- end
- end
-end
-
----Check if task should be included when saving "all" tasks to a bundle file
----@return boolean
-function Task:should_include_in_bundle()
- return self._include_in_bundle
-end
-
----@param include boolean
-function Task:set_include_in_bundle(include)
- self._include_in_bundle = include
-end
-
--- Returns the arguments require to create a clone of this task
+---Returns the arguments require to create a clone of this task when passed to overseer.new_task
---@return overseer.TaskDefinition
function Task:serialize()
local components = {}
@@ -259,22 +169,26 @@ function Task:serialize()
env = self.env,
strategy = self.strategy_defn,
components = components,
+ from_template = self.from_template,
}
end
+---Create a deep copy of this task
---@return overseer.Task
function Task:clone()
return Task.new(self:serialize())
end
+---Add a component, no-op if it already exists
+---@param comp overseer.Serialized
function Task:add_component(comp)
self:add_components({ comp })
end
+---Add components, skipping any that already exist
+---@param components overseer.Serialized[]
function Task:add_components(components)
- vim.validate({
- components = { components, "t" },
- })
+ vim.validate("components", components, "table")
local new_comps = component.resolve(components, self.components)
for _, v in ipairs(component.load(new_comps, self.default_component_params)) do
table.insert(self.components, v)
@@ -285,15 +199,16 @@ function Task:add_components(components)
end
end
+---Add component, overwriting any existing
+---@param comp overseer.Serialized
function Task:set_component(comp)
self:set_components({ comp })
end
--- Add components, overwriting any existing
+---Add components, overwriting any existing
+---@param components overseer.Serialized[]
function Task:set_components(components)
- vim.validate({
- components = { components, "t" },
- })
+ vim.validate("components", components, "table")
for _, new_comp in ipairs(component.load(components, self.default_component_params)) do
local found = false
local replaced = false
@@ -321,11 +236,9 @@ function Task:set_components(components)
end
---@param name string
----@return overseer.Component?
+---@return nil|overseer.Component
function Task:get_component(name)
- vim.validate({
- name = { name, "s" },
- })
+ vim.validate("name", name, "string")
for _, v in ipairs(self.components) do
if v.name == name then
return v
@@ -334,18 +247,16 @@ function Task:get_component(name)
end
---@param name string
+---@return nil|overseer.Component
function Task:remove_component(name)
- vim.validate({
- name = { name, "s" },
- })
- return self:remove_components({ name })
+ vim.validate("name", name, "string")
+ return self:remove_components({ name })[1]
end
---@param names string[]
+---@return overseer.Component[]
function Task:remove_components(names)
- vim.validate({
- names = { names, "t" },
- })
+ vim.validate("names", names, "table")
local lookup = {}
for _, name in ipairs(names) do
lookup[name] = true
@@ -372,16 +283,17 @@ end
---@param name string
---@return boolean
function Task:has_component(name)
- vim.validate({ name = { name, "s" } })
+ vim.validate("name", name, "string")
local new_comps = component.resolve({ name }, self.components)
return vim.tbl_isempty(new_comps)
end
---Subscribe to events on this task
----Listeners cannot be serialized, so will not be saved when saving task to disk and will not be
----copied when cloning the task.
---@param event string
----@param callback fun(task: overseer.Task, ...: any): nil|boolean Callback can return false to unsubscribe itself
+---@param callback fun(task: overseer.Task, ...: any): nil|boolean Callback can return a truthy value to unsubscribe itself
+---@note
+--- Listeners cannot be serialized, so will not be saved when saving task
+--- to disk and will not be copied when cloning the task.
function Task:subscribe(event, callback)
if not self._subscribers[event] then
self._subscribers[event] = {}
@@ -389,6 +301,7 @@ function Task:subscribe(event, callback)
table.insert(self._subscribers[event], callback)
end
+---Unsubscribe from an event that was previously subscribed to
---@param event string
---@param callback fun(task: overseer.Task, ...: any)
function Task:unsubscribe(event, callback)
@@ -402,26 +315,31 @@ function Task:unsubscribe(event, callback)
end
end
+---Returns true if the task is PENDING
---@return boolean
function Task:is_pending()
return self.status == STATUS.PENDING
end
+---Returns true if the task is RUNNING
---@return boolean
function Task:is_running()
return self.status == STATUS.RUNNING
end
+---Returns true if the task is complete (not PENDING or RUNNING)
---@return boolean
function Task:is_complete()
return self.status ~= STATUS.PENDING and self.status ~= STATUS.RUNNING
end
+---Returns true if the task is DISPOSED
---@return boolean
function Task:is_disposed()
return self.status == STATUS.DISPOSED
end
+---Get the buffer containing the task output. Will be nil if task is PENDING.
---@return number|nil
function Task:get_bufnr()
local bufnr = self.strategy:get_bufnr()
@@ -430,22 +348,16 @@ function Task:get_bufnr()
end
end
+---Open the task output in a window
---@param direction? "float"|"tab"|"vertical"|"horizontal"
+---@note
+--- You can also use get_bufnr() to get the buffer and open it however you like.
function Task:open_output(direction)
local bufnr = self:get_bufnr()
if not bufnr then
return
end
- -- Toggleterm itself needs to handle these operations.
- -- TODO: maybe we should build a formal abstraction that handles this, instead of relying on a
- -- gross if statement here.
- if self.strategy.name == "toggleterm" and direction then
- ---@diagnostic disable-next-line: undefined-field
- self.strategy:open_terminal(direction)
- return
- end
-
if direction == "float" then
local winid = layout.open_fullscreen_float(bufnr)
util.scroll_to_end(winid)
@@ -455,13 +367,24 @@ function Task:open_output(direction)
util.set_term_window_opts()
util.scroll_to_end(0)
elseif direction == "vertical" then
+ -- If we're currently in the task list or any other fixed-height panel,
+ -- open a split in the nearest other window
+ if vim.wo.winfixheight then
+ for _, winid in ipairs(util.get_fixed_wins()) do
+ if not vim.wo[winid].winfixheight then
+ util.go_win_no_au(winid)
+ break
+ end
+ end
+ end
vim.cmd.vsplit()
vim.api.nvim_win_set_buf(0, bufnr)
util.set_term_window_opts()
util.scroll_to_end(0)
elseif direction == "horizontal" then
- -- If we're currently in the task list, open a split in the nearest other window
- if vim.bo.filetype == "OverseerList" then
+ -- If we're currently in the task list or any other fixed-width side panel,
+ -- open a split in the nearest other window
+ if vim.wo.winfixwidth then
for _, winid in ipairs(util.get_fixed_wins()) do
if not vim.wo[winid].winfixwidth then
util.go_win_no_au(winid)
@@ -474,33 +397,43 @@ function Task:open_output(direction)
util.set_term_window_opts()
util.scroll_to_end(0)
else
+ -- If we're currently in the task list, open in a different window
+ if vim.bo.filetype == "OverseerList" then
+ for _, winid in ipairs(util.get_fixed_wins()) do
+ if not vim.wo[winid].winfixwidth then
+ util.go_win_no_au(winid)
+ break
+ end
+ end
+ end
vim.cmd.normal({ args = { "m'" }, bang = true })
vim.api.nvim_win_set_buf(0, bufnr)
util.scroll_to_end(0)
end
end
+---Put the task back in PENDING state.
+---@note
+--- Cannot be called on running or disposed tasks.
function Task:reset()
- if self:is_disposed() then
- error(string.format("Cannot reset %s task", self.status))
- return
- elseif self:is_running() then
+ if self:is_disposed() or self:is_running() then
error(string.format("Cannot reset %s task", self.status))
return
end
+ self.time_start = nil
+ self.time_end = nil
self.result = nil
self.exit_code = nil
self.status = STATUS.PENDING
self:dispatch("on_status", self.status)
self.strategy:reset()
- task_list.touch_task(self)
self:dispatch("on_reset")
end
---Dispatch an event to all other tasks
---@param name string
function Task:broadcast(name, ...)
- for _, task in ipairs(task_list.list_tasks()) do
+ for _, task in ipairs(task_list.list_tasks({ include_ephemeral = true })) do
if task.id ~= self.id then
task:dispatch(name, ...)
end
@@ -516,7 +449,7 @@ function Task:dispatch(name, ...)
if type(comp[name]) == "function" then
local ok, err = pcall(comp[name], comp, self, ...)
if not ok then
- log:error("Task %s dispatch %s.%s: %s", self.name, comp.name, name, err)
+ log.error("Task %s dispatch %s.%s: %s", self.name, comp.name, name, err)
elseif err ~= nil then
table.insert(ret, err)
end
@@ -527,8 +460,9 @@ function Task:dispatch(name, ...)
for _, cb in ipairs(self._subscribers[name]) do
local ok, err = pcall(cb, self, ...)
if not ok then
- log:error("Task %s dispatch callback %s: %s", self.name, name, err)
- elseif err == false then
+ log.error("Task %s dispatch callback %s: %s", self.name, name, err)
+ elseif err then
+ -- A truthy return value means unsubscribe
table.insert(to_unsub, cb)
end
end
@@ -537,23 +471,23 @@ function Task:dispatch(name, ...)
end
end
if self.id and not self:is_disposed() then
- task_list.update(self)
+ task_list.touch(self)
end
return ret
end
+---@private
---@param status overseer.Status
function Task:finalize(status)
- vim.validate({
- status = { status, "s" },
- })
+ vim.validate("status", status, "string")
if not self:is_running() then
- log:warn("Task %s cannot change status from %s to %s", self.name, self.status, status)
+ log.warn("Task %s cannot change status from %s to %s", self.name, self.status, status)
return
elseif status ~= STATUS.SUCCESS and status ~= STATUS.FAILURE and status ~= STATUS.CANCELED then
- log:error("Task %s finalize passed invalid status %s", self.name, status)
+ log.error("Task %s finalize passed invalid status %s", self.name, status)
return
end
+ self.time_end = os.time()
self.status = status
local results = self:dispatch("on_pre_result")
if not vim.tbl_isempty(results) then
@@ -567,11 +501,10 @@ function Task:finalize(status)
end
end
+---@private
---@param data? table
function Task:set_result(data)
- vim.validate({
- data = { data, "t" },
- })
+ vim.validate("data", data, "table", true)
if not self:is_running() then
return
end
@@ -579,8 +512,7 @@ function Task:set_result(data)
self:dispatch("on_result", self.result)
end
----Increment the refcount for this Task.
----Prevents it from being disposed
+---Increment the refcount for this Task, preventing it from being disposed (unless force=true)
function Task:inc_reference()
self._references = self._references + 1
end
@@ -592,16 +524,14 @@ end
---Cleans up resources, removes from task list, and deletes buffer.
---@param force? boolean When true, will dispose even with a nonzero refcount or when buffer is visible
----@return boolean disposed
+---@return boolean disposed True if task was disposed
function Task:dispose(force)
- vim.validate({
- force = { force, "b", true },
- })
+ vim.validate("force", force, "boolean", true)
if self:is_disposed() then
- return false
+ return true
end
if self._references > 0 and not force then
- log:debug("Not disposing task %s: has %d references", self.name, self._references)
+ log.debug("Not disposing task %s: has %d references", self.name, self._references)
return false
end
local bufnr = self:get_bufnr()
@@ -609,7 +539,7 @@ function Task:dispose(force)
if not force then
-- Can't dispose if the strategy bufnr is open
if bufnr_visible then
- log:debug("Not disposing task %s: buffer is visible", self.name)
+ log.debug("Not disposing task %s: buffer is visible", self.name)
return false
end
end
@@ -625,7 +555,7 @@ function Task:dispose(force)
end
self.status = STATUS.DISPOSED
self:dispatch("on_status", self.status)
- log:debug("Disposing task %s", self.name)
+ log.debug("Disposing task %s", self.name)
self.strategy:dispose()
self:dispatch("on_dispose")
task_list.remove(self)
@@ -639,11 +569,12 @@ function Task:dispose(force)
return true
end
+---Reset and re-run the task
---@param force_stop? boolean If true, restart the Task even if it is currently running
---@return boolean
function Task:restart(force_stop)
- vim.validate({ force_stop = { force_stop, "b", true } })
- log:debug("Restart task %s", self.name)
+ vim.validate("force_stop", force_stop, "boolean", true)
+ log.debug("Restart task %s", self.name)
if self:is_running() then
if force_stop then
self:stop()
@@ -658,6 +589,7 @@ function Task:restart(force_stop)
end
---Called when the task strategy exits
+---@private
---@param code number
function Task:on_exit(code)
self.exit_code = code
@@ -668,7 +600,7 @@ function Task:on_exit(code)
self:dispatch("on_exit", code)
-- We shouldn't hit this unless there is no result component or it errored
if self:is_running() then
- log:error(
+ log.error(
"Task %s did not finalize during exit. Is it missing the on_exit_set_status component?",
self.name
)
@@ -677,28 +609,30 @@ function Task:on_exit(code)
end
end
+---Start a pending task
function Task:start()
if self:is_complete() then
- log:error("Cannot start task '%s' that has completed", self.name)
+ log.error("Cannot start task '%s' that has completed", self.name)
return false
end
if self:is_disposed() then
- log:error("Cannot start task '%s' that has been disposed", self.name)
+ log.error("Cannot start task '%s' that has been disposed", self.name)
return false
end
if self:is_running() then
return false
end
if vim.tbl_contains(self:dispatch("on_pre_start"), false) then
- log:debug("Component prevented task %s from starting", self.name)
+ log.debug("Component prevented task %s from starting", self.name)
return false
end
- log:debug("Starting task %s", self.name)
+ log.debug("Starting task %s", self.name)
local ok, err = pcall(self.strategy.start, self.strategy, self)
if not ok then
- log:error("Strategy '%s' failed to start for task '%s': %s", self.strategy.name, self.name, err)
+ log.error("Strategy '%s' failed to start for task '%s': %s", self.strategy.name, self.name, err)
return false
end
+ self.time_start = os.time()
self.status = STATUS.RUNNING
self:dispatch("on_status", self.status)
self:dispatch("on_start")
@@ -713,11 +647,13 @@ function Task:start()
return true
end
+---Stop a running task
+---@return boolean stopped True if the task was stopped
function Task:stop()
if not self:is_running() then
return false
end
- log:debug("Stopping task %s", self.name)
+ log.debug("Stopping task %s", self.name)
self:finalize(STATUS.CANCELED)
self.strategy:stop()
return true
diff --git a/lua/overseer/task_bundle.lua b/lua/overseer/task_bundle.lua
deleted file mode 100644
index 0b6f1268..00000000
--- a/lua/overseer/task_bundle.lua
+++ /dev/null
@@ -1,245 +0,0 @@
-local Task = require("overseer.task")
-local config = require("overseer.config")
-local confirm = require("overseer.confirm")
-local files = require("overseer.files")
-local log = require("overseer.log")
-local task_list = require("overseer.task_list")
-local M = {}
-
-local function get_bundle_dir()
- return files.get_stdpath_filename("state", "overseer")
-end
-
-local function get_bundle_previewer()
- local ok, Previewer = pcall(require, "telescope.previewers.previewer")
- if not ok then
- return nil
- end
-
- return Previewer:new({
- title = "Task bundle",
- setup = function(self)
- return {
- bufnr = vim.api.nvim_create_buf(false, true),
- }
- end,
- teardown = function(self)
- local bufnr = self.state and self.state.bufnr
- if bufnr then
- pcall(vim.api.nvim_buf_delete, bufnr, { force = true })
- end
- end,
- preview_fn = function(self, entry, status)
- local ns = vim.api.nvim_create_namespace("overseer")
- vim.api.nvim_buf_clear_namespace(self.state.bufnr, ns, 0, -1)
- vim.api.nvim_win_set_buf(status.preview_win, self.state.bufnr)
- local lines = {}
- local highlights = {}
- local data = files.load_json_file(
- files.join(get_bundle_dir(), string.format("%s.bundle.json", entry.value))
- )
- for _, params in ipairs(data) do
- local task_ok, task = pcall(Task.new_uninitialized, params)
- if task_ok then
- task:render(lines, highlights, 3)
- end
- end
- vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, true, lines)
- for _, hl in ipairs(highlights) do
- local group, row, col_start, col_end = unpack(hl)
- vim.api.nvim_buf_add_highlight(self.state.bufnr, ns, group, row - 1, col_start, col_end)
- end
- end,
- })
-end
-
----@return string[]
-M.list_task_bundles = function()
- local bundle_dir = get_bundle_dir()
- if not files.exists(bundle_dir) then
- return {}
- end
- local filenames = files.list_files(bundle_dir)
- local ret = {}
- for _, filename in ipairs(filenames) do
- local name = filename:match("^(.+)%.bundle%.json$")
- if name then
- table.insert(ret, name)
- end
- end
- return ret
-end
-
----@param name nil|string
----@param opts nil|table
---- ignore_missing nil|boolean When true, don't notify if bundle doesn't exist
---- autostart nil|boolean When true, start the tasks after loading (default to config.bundles.autostart_on_load)
-M.load_task_bundle = function(name, opts)
- vim.validate({
- name = { name, "s", true },
- opts = { opts, "t", true },
- })
- opts = vim.tbl_deep_extend("keep", opts or {}, {
- autostart = config.bundles.autostart_on_load,
- })
- if name then
- local filepath = files.join(get_bundle_dir(), string.format("%s.bundle.json", name))
- local data = files.load_json_file(filepath)
- if not data then
- if not opts.ignore_missing then
- vim.notify(string.format("Could not find task bundle %s", name), vim.log.levels.ERROR)
- end
- return
- end
- local count = 0
- for _, params in ipairs(data) do
- local ok, task = pcall(Task.new, params)
- if ok then
- count = count + 1
- if opts.autostart then
- task:start()
- end
- else
- log:error("Could not load task in bundle %s: %s", filepath, task)
- end
- end
- vim.notify(string.format("Started %d tasks", count))
- else
- local tasks = M.list_task_bundles()
- if #tasks == 0 then
- if not opts.ignore_missing then
- vim.notify("No saved task bundles", vim.log.levels.WARN)
- end
- return
- end
- vim.ui.select(tasks, {
- prompt = "Load task bundle:",
- kind = "overseer_task_bundle",
- telescope = {
- previewer = get_bundle_previewer(),
- },
- }, function(selected)
- if selected then
- M.load_task_bundle(selected, opts)
- end
- end)
- end
-end
-
----@param name? string
----@param tasks? overseer.Task[]
----@param opts? {on_conflict?: "overwrite"|"append"|"cancel"}
-M.save_task_bundle = function(name, tasks, opts)
- vim.validate({
- name = { name, "s", true },
- tasks = { tasks, "t", true },
- opts = { opts, "t", true },
- })
- opts = opts or {}
- if name then
- local filename = string.format("%s.bundle.json", name)
- local serialized
- if tasks then
- serialized = {}
- for _, task in ipairs(tasks) do
- table.insert(serialized, task:serialize())
- end
- else
- serialized = vim.tbl_map(function(task)
- return task:serialize()
- end, task_list.list_tasks(config.bundles.save_task_opts))
- end
- if vim.tbl_isempty(serialized) then
- if opts.on_conflict == "overwrite" then
- M.delete_task_bundle(name, { ignore_missing = true })
- end
- return
- end
- local filepath = files.join(get_bundle_dir(), filename)
-
- local function append_to_file()
- local data = files.load_json_file(files.join(get_bundle_dir(), filename))
- for _, new_task in ipairs(serialized) do
- table.insert(data, new_task)
- end
- files.write_json_file(filepath, data)
- end
-
- if files.exists(filepath) then
- if opts.on_conflict == "overwrite" then
- files.write_json_file(filepath, serialized)
- elseif opts.on_conflict == "append" then
- append_to_file()
- elseif opts.on_conflict == "cancel" then
- -- Do nothing
- else
- confirm({
- message = string.format(
- "%s exists.\nWould you like to overwrite it or append to it?",
- filename
- ),
- choices = {
- "&Overwrite",
- "&Append",
- "Cancel",
- },
- default = 3,
- }, function(idx)
- if idx == 1 then
- files.write_json_file(filepath, serialized)
- elseif idx == 2 then
- append_to_file()
- end
- end)
- end
- else
- files.write_json_file(filepath, serialized)
- end
- else
- vim.ui.input({
- prompt = "Task bundle name:",
- completion = "customlist,overseer#task_bundle_completelist",
- }, function(selected)
- if selected and selected ~= "" then
- M.save_task_bundle(selected, tasks)
- end
- end)
- end
-end
-
----@param name? string
----@param opts? {ignore_missing?: boolean}
-M.delete_task_bundle = function(name, opts)
- vim.validate({
- name = { name, "s", true },
- opts = { opts, "t", true },
- })
- opts = opts or {}
- if name then
- local filename = string.format("%s.bundle.json", name)
- if not files.delete_file(files.join(get_bundle_dir(), filename)) then
- if not opts.ignore_missing then
- vim.notify(string.format("No task bundle at %s", filename))
- end
- end
- else
- local tasks = M.list_task_bundles()
- if #tasks == 0 then
- vim.notify("No saved task bundles", vim.log.levels.WARN)
- return
- end
- vim.ui.select(tasks, {
- prompt = "Delete task bundle:",
- kind = "overseer_task_bundle",
- telescope = {
- previewer = get_bundle_previewer(),
- },
- }, function(selected)
- if selected then
- M.delete_task_bundle(selected)
- end
- end)
- end
-end
-
-return M
diff --git a/lua/overseer/task_editor.lua b/lua/overseer/task_editor.lua
index ff8aa650..49246219 100644
--- a/lua/overseer/task_editor.lua
+++ b/lua/overseer/task_editor.lua
@@ -1,110 +1,33 @@
-local Task = require("overseer.task")
-local binding_util = require("overseer.binding_util")
local component = require("overseer.component")
-local config = require("overseer.config")
local form_utils = require("overseer.form.utils")
local util = require("overseer.util")
local M = {}
-local bindings = {
- {
- desc = "Show default key bindings",
- plug = "OverseerLauncher:ShowHelp",
- rhs = function(editor)
- editor.disable_close_on_leave = true
- binding_util.show_bindings("OverseerLauncher:")
- end,
- },
- {
- desc = "Submit the task",
- plug = "OverseerLauncher:Submit",
- rhs = function(editor)
- editor:submit()
- end,
- },
- {
- desc = "Cancel editing the task",
- plug = "OverseerLauncher:Cancel",
- rhs = function(editor)
- editor:cancel()
- end,
+local task_editable_params = { "cmd", "cwd" }
+---@type overseer.Params
+local task_builtin_params = {
+ -- It's kind of a hack to specify a delimiter without type = 'list'. This is
+ -- so the task editor displays nicely if the value is a list OR a string
+ cmd = { delimiter = " " },
+ cwd = {
+ optional = true,
},
}
--- Telescope-specific settings for picking a new component
-local function get_telescope_new_component(options)
- local has_telescope = pcall(require, "telescope")
- if not has_telescope then
- return
- end
-
- local themes = require("telescope.themes")
- local finders = require("telescope.finders")
- local entry_display = require("telescope.pickers.entry_display")
- local picker_opts = themes.get_dropdown()
-
- local width = vim.api.nvim_win_get_width(0) - 2
- local height = vim.api.nvim_win_get_height(0) - 2
- picker_opts.layout_config.width = function(_, max_columns, _)
- return math.min(max_columns, width)
- end
- picker_opts.layout_config.height = function(_, _, max_lines)
- return math.min(max_lines, height)
- end
-
- local max_name = 1
- for _, name in ipairs(options) do
- local len = string.len(name)
- if len > max_name then
- max_name = len
- end
- end
-
- local displayer = entry_display.create({
- separator = " ",
- items = {
- { width = max_name },
- { remaining = true },
- },
- })
-
- local function make_display(entry)
- local columns = {
- entry.value,
- }
- if entry.desc then
- table.insert(columns, { entry.desc, "Comment" })
- end
- return displayer(columns)
- end
- picker_opts.finder = finders.new_table({
- results = options,
- entry_maker = function(item)
- local comp = component.get(item)
- local ordinal = item
- local description
- if comp then
- description = comp.desc
- else
- description = component.stringify_alias(item)
- end
- if description then
- ordinal = ordinal .. " " .. description
- end
- return {
- display = make_display,
- ordinal = ordinal,
- desc = description,
- value = item,
- }
- end,
- })
- return picker_opts
-end
-
+---@class overseer.TaskEditor
+---@field cur_line? {[1]: number, [2]: string}
+---@field bufnr integer
+---@field private components overseer.ComponentDefinition[]
+---@field private ext_id_to_comp_idx_and_schema_field_name table
+---@field private task overseer.Task
+---@field private task_data table
+---@field private callback fun(task?: overseer.Task)
+---@field layout fun()
+---@field cleanup fun()
local Editor = {}
function Editor.new(task, task_cb)
+ -- Make sure the task doesn't get disposed while we're editing it
task:inc_reference()
local function callback(...)
task:dec_reference()
@@ -112,6 +35,7 @@ function Editor.new(task, task_cb)
task_cb(...)
end
end
+
local bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].swapfile = false
vim.bo[bufnr].bufhidden = "wipe"
@@ -123,19 +47,13 @@ function Editor.new(task, task_cb)
table.insert(components, vim.deepcopy(comp.params))
end
local task_data = {}
- for k in pairs(Task.params) do
+ for k in pairs(task_builtin_params) do
task_data[k] = vim.deepcopy(task[k])
end
- local autocmds = {}
- local cleanup, layout = form_utils.open_form_win(bufnr, {
- autocmds = autocmds,
- get_preferred_dim = function()
- -- TODO this is causing a lot of jumping
- end,
- })
+ local cleanup, layout = form_utils.open_form_win(bufnr, {})
vim.bo[bufnr].filetype = "OverseerForm"
- local editor = setmetatable({
+ local self = setmetatable({
cur_line = nil,
task = task,
callback = callback,
@@ -143,127 +61,115 @@ function Editor.new(task, task_cb)
components = components,
task_name = task.name,
task_data = task_data,
- line_to_comp = {},
disable_close_on_leave = false,
+ ext_id_to_comp_idx_and_schema_field_name = {},
layout = layout,
cleanup = cleanup,
- autocmds = autocmds,
}, { __index = Editor })
- binding_util.create_plug_bindings(bufnr, bindings, editor)
- for mode, user_bindings in pairs(config.task_launcher.bindings) do
- binding_util.create_bindings_to_plug(bufnr, mode, user_bindings, "OverseerLauncher:")
- end
+ vim.keymap.set({ "i", "n" }, "", function()
+ self:cancel()
+ end, { buffer = bufnr })
+ vim.keymap.set("n", "q", function()
+ self:cancel()
+ end, { buffer = bufnr })
vim.api.nvim_create_autocmd("BufWriteCmd", {
desc = "Submit on buffer write",
buffer = bufnr,
callback = function()
- editor:submit()
+ self:submit()
end,
})
- table.insert(
- editor.autocmds,
- vim.api.nvim_create_autocmd("BufLeave", {
- desc = "Close float on BufLeave",
- buffer = bufnr,
- nested = true,
- callback = function()
- if not editor.disable_close_on_leave then
- editor:cancel()
- end
- end,
- })
- )
- table.insert(
- editor.autocmds,
- vim.api.nvim_create_autocmd("BufEnter", {
- desc = "Reset disable_close_on_leave",
- buffer = bufnr,
- nested = true,
- callback = function()
- editor.disable_close_on_leave = false
- end,
- })
- )
- table.insert(
- editor.autocmds,
- vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
- desc = "Update form on change",
- buffer = bufnr,
- nested = true,
- callback = function()
- local lnum = vim.api.nvim_win_get_cursor(0)[1]
- local line = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, true)[1]
- editor.cur_line = { lnum, line }
- editor:parse()
- end,
- })
- )
- table.insert(
- editor.autocmds,
- vim.api.nvim_create_autocmd("InsertLeave", {
- desc = "Rerender form",
- buffer = bufnr,
- callback = function()
- editor:render()
- end,
- })
- )
- table.insert(
- editor.autocmds,
- vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, {
- desc = "Update form on move cursor",
- buffer = bufnr,
- nested = true,
- callback = function()
- editor:on_cursor_move()
- end,
- })
- )
- return editor
+ vim.api.nvim_create_autocmd("BufLeave", {
+ desc = "Close float on BufLeave",
+ buffer = bufnr,
+ nested = true,
+ callback = function()
+ if not self.disable_close_on_leave then
+ self:cancel()
+ end
+ end,
+ })
+ vim.api.nvim_create_autocmd("BufEnter", {
+ desc = "Reset disable_close_on_leave",
+ buffer = bufnr,
+ nested = true,
+ callback = function()
+ self.disable_close_on_leave = false
+ end,
+ })
+ vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
+ desc = "Update form on change",
+ buffer = bufnr,
+ nested = true,
+ callback = function()
+ local lnum = vim.api.nvim_win_get_cursor(0)[1]
+ local line = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, true)[1]
+ self.cur_line = { lnum, line }
+ self:parse()
+ vim.bo[self.bufnr].modified = false
+ end,
+ })
+ vim.api.nvim_create_autocmd("InsertLeave", {
+ desc = "Rerender form",
+ buffer = bufnr,
+ callback = function()
+ self:render()
+ end,
+ })
+ vim.api.nvim_create_autocmd({ "CursorMoved" }, {
+ desc = "Update form on move cursor",
+ buffer = bufnr,
+ nested = true,
+ callback = function()
+ self:on_cursor_move()
+ end,
+ })
+ return self
+end
+
+---@private
+---@return table
+function Editor:get_lnum_to_comp_idx_and_field()
+ local ns = vim.api.nvim_create_namespace("overseer")
+ local extmarks = vim.api.nvim_buf_get_extmarks(self.bufnr, ns, 0, -1, { type = "virt_text" })
+ local lnum_to_field_name = {}
+ for _, extmark in ipairs(extmarks) do
+ local ext_id, row = extmark[1], extmark[2]
+ local comp_and_field = self.ext_id_to_comp_idx_and_schema_field_name[ext_id]
+ if comp_and_field then
+ lnum_to_field_name[row + 1] = comp_and_field
+ end
+ end
+ return lnum_to_field_name
end
function Editor:on_cursor_move()
if vim.api.nvim_get_mode().mode == "i" then
return
end
- local cur = vim.api.nvim_win_get_cursor(0)
- if self.cur_line and self.cur_line[1] ~= cur[1] then
+ local lnum = vim.api.nvim_win_get_cursor(0)[1]
+ if self.cur_line and self.cur_line[1] ~= lnum then
self.cur_line = nil
self:render()
return
end
- local original_cur = vim.deepcopy(cur)
local vtext_ns = vim.api.nvim_create_namespace("overseer_vtext")
vim.api.nvim_buf_clear_namespace(self.bufnr, vtext_ns, 0, -1)
+ local lnum_to_comp_and_field = self:get_lnum_to_comp_idx_and_field()
- -- First line is task name, successive lines are task params
- -- If cursor is on the task params, make sure it's past the label
- if cur[1] > 1 and cur[1] <= #Task.ordered_params + 1 then
- local param_name = Task.ordered_params[cur[1] - 1]
- local schema = Task.params[param_name]
- local label = form_utils.render_field(schema, "", param_name, "")
- if cur[2] < string.len(label) then
- cur[2] = string.len(label)
- vim.api.nvim_win_set_cursor(0, cur)
- end
+ local comp_and_field = lnum_to_comp_and_field[lnum]
+ if not comp_and_field then
return
end
+ local comp_idx, field_name = comp_and_field[1], comp_and_field[2]
- if not self.line_to_comp[cur[1]] then
- return
- end
- local comp, param_name = unpack(self.line_to_comp[cur[1]])
-
- if param_name then
- local schema = comp.params[param_name]
- local label = form_utils.render_field(schema, " ", param_name, "")
- if cur[2] < string.len(label) then
- cur[2] = string.len(label)
- end
+ if comp_idx and field_name then
+ local comp = assert(component.get(self.components[comp_idx][1]))
+ local schema = comp.params[field_name]
if schema.desc then
- vim.api.nvim_buf_set_extmark(self.bufnr, vtext_ns, cur[1] - 1, 0, {
+ vim.api.nvim_buf_set_extmark(self.bufnr, vtext_ns, lnum - 1, 0, {
virt_text = { { schema.desc, "Comment" } },
})
end
@@ -272,70 +178,102 @@ function Editor:on_cursor_move()
or completion_schema.choices
vim.api.nvim_buf_set_var(0, "overseer_choices", choices)
end
- if cur[1] ~= original_cur[1] or cur[2] ~= original_cur[2] then
- vim.api.nvim_win_set_cursor(0, cur)
- end
end
function Editor:render()
local ns = vim.api.nvim_create_namespace("overseer")
vim.api.nvim_buf_clear_namespace(self.bufnr, ns, 0, -1)
- self.line_to_comp = {}
local lines = { self.task_name }
- local highlights = { { "OverseerTask", 1, 0, -1 } }
+ local ext_idx_to_comp_and_schema_field_name = {}
+ local extmarks = {}
+ table.insert(extmarks, {
+ 1,
+ 0,
+ { hl_group = "OverseerTask", end_col = #lines[1] },
+ })
- for _, k in ipairs(Task.ordered_params) do
- local schema = Task.params[k]
+ for _, k in ipairs(task_editable_params) do
+ local schema = task_builtin_params[k]
local value = self.task_data[k]
- table.insert(lines, form_utils.render_field(schema, "", k, value))
- if form_utils.validate_field(schema, value) then
- table.insert(highlights, { "OverseerField", #lines, 0, string.len(k) })
- else
- table.insert(highlights, { "DiagnosticError", #lines, 0, string.len(k) })
- end
+ table.insert(lines, tostring(form_utils.render_value(schema, value)))
+ local hl = form_utils.validate_field(schema, value) and "OverseerField" or "DiagnosticError"
+ table.insert(extmarks, {
+ #lines,
+ 0,
+ {
+ virt_text = { { k, hl }, { ": ", "NormalFloat" } },
+ virt_text_pos = "inline",
+ undo_restore = false,
+ invalidate = true,
+ },
+ })
+ ext_idx_to_comp_and_schema_field_name[#extmarks] = { nil, k }
end
- for _, params in ipairs(self.components) do
+ for i, params in ipairs(self.components) do
local comp = assert(component.get(params[1]))
- local line = comp.name
- table.insert(highlights, { "OverseerComponent", #lines + 1, 0, string.len(comp.name) })
+ table.insert(lines, "")
+ local desc
if comp.desc then
- local prev_len = string.len(line)
- line = string.format("%s (%s)", line, comp.desc)
- table.insert(highlights, { "Comment", #lines + 1, prev_len + 1, -1 })
+ desc = { string.format(" (%s)", comp.desc), "Comment" }
end
- table.insert(lines, line)
- self.line_to_comp[#lines] = { comp, nil }
+ table.insert(extmarks, {
+ #lines,
+ 0,
+ {
+ virt_text = { { comp.name, "OverseerComponent" }, desc },
+ virt_text_pos = "overlay",
+ invalidate = true,
+ undo_restore = false,
+ },
+ })
+ ext_idx_to_comp_and_schema_field_name[#extmarks] = { i, nil }
local schema = comp.params
if schema then
for k, param_schema in pairs(schema) do
local value = params[k]
- table.insert(lines, form_utils.render_field(param_schema, " ", k, value))
- if form_utils.validate_field(param_schema, value) then
- table.insert(highlights, { "OverseerField", #lines, 0, 2 + string.len(k) })
- else
- table.insert(highlights, { "DiagnosticError", #lines, 0, 2 + string.len(k) })
+ table.insert(lines, tostring(form_utils.render_value(param_schema, value)))
+ local field_hl = "OverseerField"
+ if not form_utils.validate_field(param_schema, value) then
+ field_hl = "DiagnosticError"
end
- self.line_to_comp[#lines] = { comp, k }
+ table.insert(extmarks, {
+ #lines,
+ 0,
+ {
+ virt_text = { { " " .. k, field_hl }, { ": ", "NormalFloat" } },
+ virt_text_pos = "inline",
+ undo_restore = false,
+ invalidate = true,
+ },
+ })
+ ext_idx_to_comp_and_schema_field_name[#extmarks] = { i, k }
end
end
end
+
+ -- When in insert mode, don't overwrite whatever in-progress value the user is typing
if self.cur_line and vim.api.nvim_get_mode().mode == "i" then
local lnum, line = unpack(self.cur_line)
lines[lnum] = line
end
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, true, lines)
- util.add_highlights(self.bufnr, ns, highlights)
if self.task_name:match("^%s*$") then
vim.api.nvim_buf_set_extmark(self.bufnr, ns, 0, 0, {
virt_text = { { "Task name is required", "DiagnosticError" } },
})
end
+ for i, mark in ipairs(extmarks) do
+ local lnum, col, opts = unpack(mark)
+ local ext_id = vim.api.nvim_buf_set_extmark(self.bufnr, ns, lnum - 1, col, opts)
+ self.ext_id_to_comp_idx_and_schema_field_name[ext_id] = ext_idx_to_comp_and_schema_field_name[i]
+ end
self:on_cursor_move()
end
+---@param insert_position integer
function Editor:add_new_component(insert_position)
self.disable_close_on_leave = true
self.cur_line = nil
@@ -345,6 +283,7 @@ function Editor:add_new_component(insert_position)
end
local options = {}
+ local longest_option = 1
local existing = {}
for _, comp in ipairs(self.components) do
existing[comp[1]] = true
@@ -352,11 +291,13 @@ function Editor:add_new_component(insert_position)
for _, v in ipairs(component.list_editable()) do
if not existing[v] then
table.insert(options, v)
+ longest_option = math.max(longest_option, vim.api.nvim_strwidth(v))
end
end
for _, v in ipairs(component.list_aliases()) do
if not v:match("^default") then
table.insert(options, v)
+ longest_option = math.max(longest_option, vim.api.nvim_strwidth(v))
end
end
table.sort(options)
@@ -365,18 +306,18 @@ function Editor:add_new_component(insert_position)
prompt = "New component",
kind = "overseer_new_component",
format_item = function(item)
+ local name = util.align(item, longest_option, "left")
local comp = component.get(item)
if comp then
if comp.desc then
- return string.format("%s - %s", item, comp.desc)
+ return string.format("%s %s", name, comp.desc)
else
return item
end
else
- return string.format("%s [%s]", item, component.stringify_alias(item))
+ return string.format("%s [%s]", name, component.stringify_alias(item))
end
end,
- telescope = get_telescope_new_component(options),
}, function(result)
self.disable_close_on_leave = false
if result then
@@ -389,7 +330,7 @@ function Editor:add_new_component(insert_position)
else
compdef = vim.tbl_deep_extend("force", component.create_default_params(v[1]), v)
end
- table.insert(self.components, insert_position - 1 + i, compdef)
+ table.insert(self.components, insert_position + i, compdef)
end
else
local params = component.create_default_params(result)
@@ -401,68 +342,47 @@ function Editor:add_new_component(insert_position)
end
function Editor:parse()
- self.task_name = vim.api.nvim_buf_get_lines(self.bufnr, 0, 1, true)[1]
- local offset = 1
- local buflines = vim.api.nvim_buf_get_lines(self.bufnr, offset, -1, true)
- local comp_map = {}
- local comp_idx = {}
- for i, v in ipairs(self.components) do
- comp_map[v[1]] = v
- comp_idx[v[1]] = i
+ local lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, true)
+ self.task_name = lines[1]
+ local lnum_to_comp_and_field = self:get_lnum_to_comp_idx_and_field()
+ if vim.tbl_isempty(lnum_to_comp_and_field) then
+ -- user may have done an "undo" and removed all the extmarks
+ self:render()
+ return
end
- local insert_position = #self.components + 1
- for i, line in ipairs(buflines) do
- if line:match("^%s*$") then
- local comp = self.line_to_comp[i + offset]
- if comp then
- insert_position = comp_idx[comp[1].name]
- elseif i < #buflines / 2 then
- insert_position = 1
- end
- self:add_new_component(insert_position)
+ local new_comp_insert_pos = 1
+ local seen_components = {}
+ -- Skip the first line, which is the task name
+ for i = 2, #lines do
+ local line = lines[i]
+ local comp_and_field = lnum_to_comp_and_field[i]
+ -- If this line doesn't map to a task param, component name, or component field, then it is a
+ -- new blank line and we should insert a new component
+ if not comp_and_field then
+ self:add_new_component(new_comp_insert_pos)
return
end
- end
-
- local seen_comps = {}
- local comp
- local last_idx = 0
- for _, line in ipairs(buflines) do
- local prefix, name, text = line:match("^(%s*)([^%s]+): ?(.*)$")
- if name and comp and prefix == " " then
- local param_schema = comp.params[name]
- if param_schema then
- local parsed, value = form_utils.parse_value(param_schema, text)
- if parsed then
- comp_map[comp.name][name] = value
- end
+ local comp_idx, field_name = comp_and_field[1], comp_and_field[2]
+
+ if not comp_idx and field_name then
+ -- This is a task param
+ local param_schema = task_builtin_params[field_name]
+ local parsed, value = form_utils.parse_value(param_schema, line)
+ if parsed then
+ self.task_data[field_name] = value
end
- elseif name and prefix == "" then
- local param_schema = Task.params[name]
- if param_schema then
- local parsed, value = form_utils.parse_value(param_schema, text)
- if parsed then
- self.task_data[name] = value
- end
- end
- else
- local comp_name = line:match("^([^%s]+) ")
- if comp_name then
- comp = component.get(comp_name)
- if comp then
- if not comp_map[comp_name] then
- -- This is a new component we need to insert
- last_idx = last_idx + 1
- local params = component.create_default_params(comp_name)
- comp_map[comp_name] = params
- comp_idx[comp_name] = last_idx
- table.insert(self.components, last_idx, params)
- else
- last_idx = comp_idx[comp_name]
- end
- seen_comps[comp_name] = true
- end
+ elseif comp_idx and not field_name then
+ local comp_name = self.components[comp_idx][1]
+ new_comp_insert_pos = new_comp_insert_pos + 1
+ seen_components[comp_name] = true
+ elseif comp_idx and field_name then
+ local comp_data = self.components[comp_idx]
+ local comp = assert(component.get(comp_data[1]))
+ local param_schema = comp.params[field_name]
+ local parsed, value = form_utils.parse_value(param_schema, line)
+ if parsed then
+ comp_data[field_name] = value
end
end
end
@@ -470,7 +390,7 @@ function Editor:parse()
-- Remove all the components that we didn't see
local to_remove = {}
for i, v in ipairs(self.components) do
- if not seen_comps[v[1]] then
+ if not seen_components[v[1]] then
table.insert(to_remove, 1, i)
end
end
@@ -503,6 +423,7 @@ function Editor:submit()
return c[1]
end, self.components))
local to_remove = {}
+ ---@diagnostic disable-next-line: invisible
for _, v in ipairs(self.task.components) do
if not seen[v.name] then
table.insert(to_remove, v.name)
diff --git a/lua/overseer/task_list/actions.lua b/lua/overseer/task_list/actions.lua
index 0cf938d0..6702f1d9 100644
--- a/lua/overseer/task_list/actions.lua
+++ b/lua/overseer/task_list/actions.lua
@@ -1,7 +1,6 @@
local component = require("overseer.component")
local constants = require("overseer.constants")
local form = require("overseer.form")
-local task_bundle = require("overseer.task_bundle")
local task_editor = require("overseer.task_editor")
local task_list = require("overseer.task_list")
local util = require("overseer.util")
@@ -9,6 +8,12 @@ local STATUS = constants.STATUS
local M
+---@class (exact) overseer.Action
+---@field desc? string Detailed description of what the action does
+---@field condition? fun(task: overseer.Task): boolean Function to check if the action is applicable
+---@field run fun(task: overseer.Task)
+
+---@type table
M = {
start = {
condition = function(task)
@@ -26,12 +31,6 @@ M = {
task:stop()
end,
},
- save = {
- desc = "save the task to a bundle file",
- run = function(task)
- task_bundle.save_task_bundle(nil, { task })
- end,
- },
restart = {
condition = function(task)
return task.status ~= STATUS.PENDING
@@ -46,10 +45,11 @@ M = {
end,
},
edit = {
+ desc = "Edit the task components directly",
run = function(task)
task_editor.open(task, function(t)
if t then
- task_list.update(t)
+ task_list.touch(t)
end
end)
end,
@@ -91,7 +91,7 @@ M = {
end
params[1] = "restart_on_save"
task:set_component(params)
- task_list.update(task)
+ task_list.touch(task)
end)
end,
},
@@ -107,7 +107,7 @@ M = {
["open float"] = {
desc = "open terminal in a floating window",
condition = function(task)
- return task:get_bufnr()
+ return task:get_bufnr() ~= nil
end,
run = function(task)
task:open_output("float")
@@ -116,7 +116,7 @@ M = {
open = {
desc = "open terminal in the current window",
condition = function(task)
- return task:get_bufnr()
+ return task:get_bufnr() ~= nil
end,
run = function(task)
task:open_output()
@@ -125,7 +125,7 @@ M = {
["open hsplit"] = {
desc = "open terminal in a horizontal split",
condition = function(task)
- return task:get_bufnr()
+ return task:get_bufnr() ~= nil
end,
run = function(task)
task:open_output("horizontal")
@@ -134,7 +134,7 @@ M = {
["open vsplit"] = {
desc = "open terminal in a vertical split",
condition = function(task)
- return task:get_bufnr()
+ return task:get_bufnr() ~= nil
end,
run = function(task)
task:open_output("vertical")
@@ -143,7 +143,7 @@ M = {
["open tab"] = {
desc = "open terminal in a new tab",
condition = function(task)
- return task:get_bufnr()
+ return task:get_bufnr() ~= nil
end,
run = function(task)
task:open_output("tab")
@@ -152,7 +152,7 @@ M = {
["set quickfix diagnostics"] = {
desc = "put the diagnostics results into quickfix",
condition = function(task)
- return task.result
+ return task.result ~= nil
and task.result.diagnostics
and not vim.tbl_isempty(task.result.diagnostics)
end,
@@ -163,7 +163,7 @@ M = {
["set loclist diagnostics"] = {
desc = "put the diagnostics results into loclist",
condition = function(task)
- return task.result
+ return task.result ~= nil
and task.result.diagnostics
and not vim.tbl_isempty(task.result.diagnostics)
end,
@@ -177,17 +177,17 @@ M = {
condition = function(task)
local bufnr = task:get_bufnr()
return task:is_complete()
- and bufnr
+ and bufnr ~= nil
and vim.api.nvim_buf_is_valid(bufnr)
and vim.api.nvim_buf_is_loaded(bufnr)
end,
run = function(task)
- local lines = vim.api.nvim_buf_get_lines(task:get_bufnr(), 0, -1, true)
+ local lines = vim.api.nvim_buf_get_lines(assert(task:get_bufnr()), 0, -1, true)
vim.fn.setqflist({}, " ", {
title = task.name,
- context = task.name,
lines = lines,
-- Peep into the default component params to fetch the errorformat
+ ---@diagnostic disable-next-line: invisible
efm = task.default_component_params.errorformat,
})
vim.cmd("botright copen")
diff --git a/lua/overseer/task_list/bindings.lua b/lua/overseer/task_list/bindings.lua
deleted file mode 100644
index 355d6c08..00000000
--- a/lua/overseer/task_list/bindings.lua
+++ /dev/null
@@ -1,161 +0,0 @@
-local binding_util = require("overseer.binding_util")
-local M
-M = {
- {
- desc = "Show default key bindings",
- plug = "OverseerTask:ShowHelp",
- rhs = function()
- binding_util.show_bindings("OverseerTask:")
- end,
- },
- {
- desc = "Open task action menu",
- plug = "OverseerTask:RunAction",
- rhs = function(sidebar)
- sidebar:run_action()
- end,
- },
- {
- desc = "Edit task",
- plug = "OverseerTask:Edit",
- rhs = function(sidebar)
- sidebar:run_action("edit")
- end,
- },
- {
- desc = "Open task terminal in current window",
- plug = "OverseerTask:Open",
- rhs = function(sidebar)
- sidebar:run_action("open")
- end,
- },
- {
- desc = "Open task terminal in a split",
- plug = "OverseerTask:OpenSplit",
- rhs = function(sidebar)
- sidebar:run_action("open hsplit")
- end,
- },
- {
- desc = "Open task terminal in a vsplit",
- plug = "OverseerTask:OpenVsplit",
- rhs = function(sidebar)
- sidebar:run_action("open vsplit")
- end,
- },
- {
- desc = "Open task terminal in a floating window",
- plug = "OverseerTask:OpenFloat",
- rhs = function(sidebar)
- sidebar:run_action("open float")
- end,
- },
- {
- desc = "Open task output in a quickfix window",
- plug = "OverseerTask:OpenQuickFix",
- rhs = function(sidebar)
- sidebar:run_action("open output in quickfix")
- end,
- },
- {
- desc = "Toggle task terminal in a preview window",
- plug = "OverseerTask:TogglePreview",
- rhs = function(sidebar)
- sidebar:toggle_preview()
- end,
- },
- {
- desc = "Increase task detail level",
- plug = "OverseerTask:IncreaseDetail",
- rhs = function(sidebar)
- sidebar:change_task_detail(1)
- end,
- },
- {
- desc = "Decrease task detail level",
- plug = "OverseerTask:DecreaseDetail",
- rhs = function(sidebar)
- sidebar:change_task_detail(-1)
- end,
- },
- {
- desc = "Increase all task detail levels",
- plug = "OverseerTask:IncreaseAllDetail",
- rhs = function(sidebar)
- sidebar:change_default_detail(1)
- end,
- },
- {
- desc = "Decrease all task detail levels",
- plug = "OverseerTask:DecreaseAllDetail",
- rhs = function(sidebar)
- sidebar:change_default_detail(-1)
- end,
- },
- {
- desc = "Decrease window width",
- plug = "OverseerTask:DecreaseWidth",
- rhs = function()
- local width = vim.api.nvim_win_get_width(0)
- vim.api.nvim_win_set_width(0, math.max(10, width - 10))
- end,
- },
- {
- desc = "Increase window width",
- plug = "OverseerTask:IncreaseWidth",
- rhs = function()
- local width = vim.api.nvim_win_get_width(0)
- vim.api.nvim_win_set_width(0, math.max(10, width + 10))
- end,
- },
- {
- desc = "Jump to previous task",
- plug = "OverseerTask:PrevTask",
- rhs = function(sidebar)
- sidebar:jump(-1)
- end,
- },
- {
- desc = "Jump to next task",
- plug = "OverseerTask:NextTask",
- rhs = function(sidebar)
- sidebar:jump(1)
- end,
- },
- {
- desc = "Scroll up in the task output window",
- plug = "OverseerTask:ScrollOutputUp",
- rhs = function(sidebar)
- sidebar:scroll_output(-1)
- end,
- },
- {
- desc = "Scroll down in the task output window",
- plug = "OverseerTask:ScrollOutputDown",
- rhs = function(sidebar)
- sidebar:scroll_output(1)
- end,
- },
- {
- desc = "Close window",
- plug = "OverseerTask:Close",
- rhs = function()
- vim.cmd("close")
- end,
- },
- {
- desc = "Dispose task",
- plug = "OverseerTask:Dispose",
- rhs = function(sidebar)
- sidebar:run_action("dispose")
- end,
- },
- {
- desc = "Stop task",
- plug = "OverseerTask:Stop",
- rhs = function(sidebar)
- sidebar:run_action("stop")
- end,
- },
-}
-return M
diff --git a/lua/overseer/task_list/init.lua b/lua/overseer/task_list/init.lua
index d55edcab..74a9bca8 100644
--- a/lua/overseer/task_list/init.lua
+++ b/lua/overseer/task_list/init.lua
@@ -1,8 +1,12 @@
local util = require("overseer.util")
local M = {}
+---@type overseer.Task[]
local tasks = {}
+---@type table
local lookup = {}
+---@type table
+local sorted_cache = {}
---@return integer
M.get_or_create_bufnr = function()
@@ -10,54 +14,70 @@ M.get_or_create_bufnr = function()
return sidebar.bufnr
end
-M.rerender = function()
+local function dispatch()
+ sorted_cache = {}
vim.api.nvim_exec_autocmds("User", { pattern = "OverseerListUpdate", modeline = false })
end
-local function group_parents_and_children()
- local order = {}
- for i, task in ipairs(tasks) do
- order[task.id] = i
+---@param sort? fun(a: overseer.Task, b: overseer.Task): boolean Function that sorts tasks
+local function get_sorted_tasks(sort)
+ if not sort then
+ sort = M.sort_newest_first
end
+ if sorted_cache[sort] then
+ return sorted_cache[sort]
+ end
+
+ local child_groups = {}
+ local top_level = {}
for _, task in ipairs(tasks) do
if task.parent_id then
- order[task.id] = order[task.parent_id] - 0.5
+ local group = child_groups[task.parent_id]
+ if not group then
+ group = {}
+ child_groups[task.parent_id] = group
+ end
+ table.insert(group, task)
+ else
+ table.insert(top_level, task)
+ end
+ end
+
+ table.sort(top_level, sort)
+ for _, children in pairs(child_groups) do
+ table.sort(children, sort)
+ end
+
+ local ret = {}
+ for _, task in ipairs(top_level) do
+ table.insert(ret, task)
+ local children = child_groups[task.id]
+ if children then
+ for _, child in ipairs(children) do
+ table.insert(ret, child)
+ end
end
end
- table.sort(tasks, function(a, b)
- return order[a.id] < order[b.id]
- end)
+
+ sorted_cache[sort] = ret
+ return ret
end
+---Trigger a re-render without re-sorting the tasks
---@param task? overseer.Task
-M.update = function(task)
- if not task then
- M.rerender()
- return
- end
- if task:is_disposed() then
+M.touch = function(task)
+ if not task or task:is_disposed() then
return
end
if not lookup[task.id] then
lookup[task.id] = task
table.insert(tasks, task)
- group_parents_and_children()
end
- M.rerender()
+ dispatch()
end
----@param task overseer.Task
-M.touch_task = function(task)
- if not lookup[task.id] then
- return
- end
- local idx = util.tbl_index(tasks, task.id, function(t)
- return t.id
- end)
- table.remove(tasks, idx)
- table.insert(tasks, task)
- group_parents_and_children()
- M.rerender()
+M.on_task_updated = function()
+ dispatch()
end
---@param task overseer.Task
@@ -69,7 +89,7 @@ M.remove = function(task)
break
end
end
- M.rerender()
+ dispatch()
end
---@param id number
@@ -88,51 +108,36 @@ M.get_by_name = function(name)
end
end
--- 1-indexed, most recent first
----@param index number
----@return overseer.Task|nil
-M.get_by_index = function(index)
- return tasks[#tasks + 1 - index]
-end
-
----@class overseer.ListTaskOpts
+---@class (exact) overseer.ListTaskOpts
---@field unique? boolean Deduplicates non-running tasks by name
----@field name? string|string[] Only list tasks with this name or names
----@field name_not? boolean Invert the name search (tasks *without* that name)
---@field status? overseer.Status|overseer.Status[] Only list tasks with this status or statuses
----@field status_not? boolean Invert the status search
----@field recent_first? boolean The most recent tasks are first in the list
----@field bundleable? boolean Only list tasks that should be included in a bundle
----@field filter? fun(task: overseer.Task): boolean
+---@field include_ephemeral? boolean Include ephemeral tasks
+---@field wrapped? boolean Include tasks that were created by the jobstart/vim.system wrappers
+---@field filter? fun(task: overseer.Task): boolean Only include tasks where this function returns true
+---@field sort? fun(a: overseer.Task, b: overseer.Task): boolean Function that sorts tasks
---@param opts? overseer.ListTaskOpts
---@return overseer.Task[]
M.list_tasks = function(opts)
opts = opts or {}
- vim.validate({
- unique = { opts.unique, "b", true },
- -- name is string or list
- name_not = { opts.name_not, "b", true },
- -- status is string or list
- status_not = { opts.status_not, "b", true },
- recent_first = { opts.recent_first, "b", true },
- bundleable = { opts.bundleable, "b", true },
- filter = { opts.filter, "f", true },
- })
- local name = util.list_to_map(opts.name or {})
+ vim.validate("unique", opts.unique, "boolean", true)
+ vim.validate("status", opts.status, function(n)
+ return type(n) == "string" or type(n) == "table"
+ end, true)
+ vim.validate("wrapped", opts.wrapped, "boolean", true)
+ vim.validate("include_ephemeral", opts.include_ephemeral, "boolean", true)
+ vim.validate("filter", opts.filter, "function", true)
+ vim.validate("sort", opts.sort, "function", true)
+
local status = util.list_to_map(opts.status or {})
local seen = {}
local ret = {}
- for _, task in ipairs(tasks) do
+ for _, task in ipairs(get_sorted_tasks(opts.sort)) do
if
- (
- not opts.name
- or (name[task.name] and not opts.name_not)
- or (not name[task.name] and opts.name_not)
- )
- and (not opts.status or (status[task.status] and not opts.status_not) or (not status[task.status] and opts.status_not))
- and (not opts.bundleable or task:should_include_in_bundle())
+ (not opts.status or status[task.status])
+ and (opts.include_ephemeral or not task.ephemeral)
and (not opts.filter or opts.filter(task))
+ and (opts.wrapped or not task.source)
then
local idx = seen[task.name]
if idx and opts.unique then
@@ -150,10 +155,69 @@ M.list_tasks = function(opts)
end
end
end
- if opts.recent_first then
- util.tbl_reverse(ret)
- end
return ret
end
+---General purpose sort by status and start/end time
+---@param a overseer.Task
+---@param b overseer.Task
+---@return boolean
+M.default_sort = function(a, b)
+ if (a.time_end == nil) ~= (b.time_end == nil) then
+ -- If only one of the tasks has already ended, put the other one first
+ return a.time_end == nil
+ elseif (a.time_start ~= nil) ~= (b.time_start ~= nil) then
+ -- If only one of the tasks has started, put the other one first
+ return a.time_start == nil
+ elseif a.time_end ~= nil and a.time_end ~= b.time_end then
+ -- If both tasks have ended, sort by end time (most recent first)
+ return a.time_end > b.time_end
+ elseif a.time_start ~= nil and a.time_start ~= b.time_start then
+ -- If both tasks have started, sort by start time (most recent first)
+ return a.time_start > b.time_start
+ end
+
+ -- fall back to sort by name
+ return a.name < b.name
+end
+
+---A sort function that puts the newest tasks first
+---@param a overseer.Task
+---@param b overseer.Task
+---@return boolean
+M.sort_newest_first = function(a, b)
+ if a.time_start == nil then
+ if b.time_start == nil then
+ -- fall back to sort by name
+ return a.name < b.name
+ else
+ return true
+ end
+ elseif b.time_start == nil then
+ return false
+ end
+
+ -- Sort newest first
+ return a.time_start > b.time_start
+end
+
+---A sort function that puts tasks that finished most recently first
+---@param a overseer.Task
+---@param b overseer.Task
+---@return boolean
+M.sort_finished_recently = function(a, b)
+ if (a.time_end == nil) ~= (b.time_end == nil) then
+ return a.time_end ~= nil
+ elseif a.time_end ~= nil and a.time_end ~= b.time_end then
+ return a.time_end > b.time_end
+ elseif (a.time_start == nil) ~= (b.time_start == nil) then
+ return a.time_start ~= nil
+ elseif a.time_start ~= nil and a.time_start ~= b.time_start then
+ return a.time_start > b.time_start
+ else
+ -- fall back to sort by name
+ return a.name < b.name
+ end
+end
+
return M
diff --git a/lua/overseer/task_list/keymaps.lua b/lua/overseer/task_list/keymaps.lua
new file mode 100644
index 00000000..16efab0d
--- /dev/null
+++ b/lua/overseer/task_list/keymaps.lua
@@ -0,0 +1,121 @@
+local M = {}
+
+---@return overseer.Sidebar
+local function get_sidebar()
+ return assert(require("overseer.task_list.sidebar").get())
+end
+
+M.show_help = {
+ desc = "Show default keymaps",
+ callback = function()
+ local config = require("overseer.config")
+ require("overseer.keymap_util").show_help(config.task_list.keymaps)
+ end,
+}
+
+M.run_action = {
+ desc = "Run an action on the current task",
+ callback = function(opts)
+ opts = opts or {}
+ local sb = get_sidebar()
+ sb:run_action(opts.action)
+ end,
+ parameters = {
+ action = {
+ type = "string",
+ desc = "Run an action on the current task",
+ },
+ },
+}
+
+M.open = {
+ desc = "Open task output",
+ callback = function(opts)
+ opts = opts or {}
+ local sb = get_sidebar()
+ if opts.dir == "split" then
+ sb:run_action("open hsplit")
+ elseif opts.dir == "vsplit" then
+ sb:run_action("open vsplit")
+ elseif opts.dir == "tab" then
+ sb:run_action("open tab")
+ elseif opts.dir == "float" then
+ sb:run_action("open float")
+ else
+ sb:run_action("open")
+ end
+ end,
+ parameters = {
+ dir = {
+ type = '"split"|"vsplit"|"tab"|"float"',
+ desc = "type of window to open the task output in",
+ },
+ },
+}
+
+M.prev_task = {
+ desc = "Jump to previous task",
+ callback = function()
+ local sb = get_sidebar()
+ sb:jump(-1)
+ end,
+}
+
+M.next_task = {
+ desc = "Jump to next task",
+ callback = function()
+ local sb = get_sidebar()
+ sb:jump(1)
+ end,
+}
+
+M.scroll_output_up = {
+ desc = "Scroll up in the task output window",
+ callback = function()
+ local sb = get_sidebar()
+ sb:scroll_output(-1)
+ end,
+}
+
+M.scroll_output_down = {
+ desc = "Scroll down in the task output window",
+ callback = function()
+ local sb = get_sidebar()
+ sb:scroll_output(1)
+ end,
+}
+
+M.toggle_preview = {
+ desc = "Toggle task output in a preview floating window",
+ callback = function()
+ local sb = get_sidebar()
+ sb:toggle_preview()
+ end,
+}
+
+M.toggle_show_wrapped = {
+ desc = "Toggle showing wrapped builtin jobstart/vim.system tasks",
+ callback = function()
+ local sb = get_sidebar()
+ sb:toggle_show_wrapped()
+ end,
+}
+
+---List keymaps for documentation generation
+---@private
+M._get_keymaps = function()
+ local ret = {}
+ for name, keymap in pairs(M) do
+ if type(keymap) == "table" and keymap.desc then
+ table.insert(ret, {
+ name = name,
+ desc = keymap.desc,
+ deprecated = keymap.deprecated,
+ parameters = keymap.parameters,
+ })
+ end
+ end
+ return ret
+end
+
+return M
diff --git a/lua/overseer/task_list/sidebar.lua b/lua/overseer/task_list/sidebar.lua
index 96f7ef06..26e5dcb5 100644
--- a/lua/overseer/task_list/sidebar.lua
+++ b/lua/overseer/task_list/sidebar.lua
@@ -1,8 +1,7 @@
local TaskView = require("overseer.task_view")
local action_util = require("overseer.action_util")
-local binding_util = require("overseer.binding_util")
-local bindings = require("overseer.task_list.bindings")
local config = require("overseer.config")
+local keymap_util = require("overseer.keymap_util")
local layout = require("overseer.layout")
local task_list = require("overseer.task_list")
local util = require("overseer.util")
@@ -11,35 +10,35 @@ local M = {}
---@class overseer.Sidebar
---@field bufnr integer
----@field default_detail integer
----@field private task_lines {[1]: integer, [2]: overseer.Task}[]
----@field private task_detail table
+---@field private task_lines {[1]: integer, [2]: integer, [3]: overseer.Task}[]
---@field private preview? overseer.TaskView
---@field private focused_task_id? integer
+---@field private list_task_opts overseer.ListTaskOpts
local Sidebar = {}
local ref
+---@return overseer.Sidebar
+---@return boolean
M.get_or_create = function()
local sb = M.get()
local created = not sb
if not sb then
- ref = Sidebar.new()
- sb = ref
- sb:render(task_list.list_tasks())
+ sb = Sidebar.new()
+ ref = sb
+ sb:render()
end
return sb, created
end
+---@return nil|overseer.Sidebar
M.get = function()
if ref and vim.api.nvim_buf_is_loaded(ref.bufnr) and vim.api.nvim_buf_is_valid(ref.bufnr) then
return ref
end
end
-local MIN_DETAIL = 1
-local MAX_DETAIL = 3
-
+---@return overseer.Sidebar
function Sidebar.new()
local bufnr = vim.api.nvim_create_buf(false, true)
@@ -52,12 +51,12 @@ function Sidebar.new()
local self = setmetatable({
bufnr = bufnr,
- default_detail = config.task_list.default_detail,
- task_detail = {},
task_lines = {},
preview = nil,
+ list_task_opts = { include_ephemeral = true, sort = config.task_list.sort },
}, { __index = Sidebar })
self:init()
+ ---@cast self overseer.Sidebar
return self
end
@@ -89,12 +88,24 @@ function Sidebar:init()
pattern = "OverseerListUpdate",
desc = "[Overseer] Update task list when tasks change",
callback = function()
- self:render(task_list.list_tasks())
+ self:render()
end,
})
- binding_util.create_plug_bindings(self.bufnr, bindings, self)
- binding_util.create_bindings_to_plug(self.bufnr, "n", config.task_list.bindings, "OverseerTask:")
+ local periodic_update
+ periodic_update = function()
+ if not vim.api.nvim_buf_is_valid(self.bufnr) then
+ return
+ end
+ -- only rerender if the buffer is visible
+ if self:get_winid() then
+ self:render()
+ end
+ vim.defer_fn(periodic_update, 1000) -- update every second
+ end
+ periodic_update()
+
+ keymap_util.set_keymaps(config.task_list.keymaps, self.bufnr)
end
---@private
@@ -134,43 +145,44 @@ end
---@private
---@return nil|overseer.Task
+---@return nil|integer offset
function Sidebar:get_task_from_line(lnum)
if not lnum then
local winid = self:get_winid()
if not winid then
- return nil
+ return nil, nil
end
lnum = vim.api.nvim_win_get_cursor(winid)[1]
end
for _, v in ipairs(self.task_lines) do
- local end_lnum, task = v[1], v[2]
- if end_lnum >= lnum then
- return task
+ local start_lnum, last_lnum, task = v[1], v[2], v[3]
+ if lnum <= last_lnum then
+ return task, lnum - start_lnum
end
end
end
---@param task_id integer
-function Sidebar:focus_task_id(task_id)
+---@param offset? integer
+function Sidebar:focus_task_id(task_id, offset)
local winid = self:get_winid()
if not winid then
return
end
- local start_lnum = 1
+ offset = offset or 0
for _, v in ipairs(self.task_lines) do
- local end_lnum, task = v[1], v[2]
+ local start_line, task = v[1], v[3]
if task.id == task_id then
- vim.api.nvim_win_set_cursor(winid, { start_lnum, 0 })
+ pcall(vim.api.nvim_win_set_cursor, winid, { start_line + offset, 0 })
self:set_task_focused(task_id)
return
end
- start_lnum = end_lnum + 2
end
end
---@param bufnr integer
----@param winlayout nil|any
+---@param winlayout? vim.fn.winlayout.branch|vim.fn.winlayout.leaf|vim.fn.winlayout.empty
---@return nil|"left"|"right"|"bottom"
local function detect_direction(bufnr, winlayout)
if not winlayout then
@@ -178,12 +190,14 @@ local function detect_direction(bufnr, winlayout)
end
local type = winlayout[1]
if type == "leaf" then
+ ---@cast winlayout vim.fn.winlayout.leaf
if vim.api.nvim_win_get_buf(winlayout[2]) == bufnr then
return "left"
else
return nil
end
else
+ ---@cast winlayout vim.fn.winlayout.branch
for i, nested in ipairs(winlayout[2]) do
local dir = detect_direction(bufnr, nested)
if dir then
@@ -198,7 +212,7 @@ local function detect_direction(bufnr, winlayout)
end
function Sidebar:toggle_preview()
- if self.preview and not self.preview:is_disposed() then
+ if self.preview and not self.preview:is_win_closed() then
self.preview:dispose()
return
end
@@ -217,7 +231,6 @@ function Sidebar:toggle_preview()
local col = (direction == "left" and (win_width + padding) or padding)
local winid = vim.api.nvim_open_win(0, false, {
relative = "editor",
- border = config.task_win.border,
row = 1,
col = col,
width = width,
@@ -230,6 +243,7 @@ function Sidebar:toggle_preview()
end
self.preview = TaskView.new(winid, {
close_on_list_close = true,
+ list_task_opts = self.list_task_opts,
select = function(_, tasks, task_under_cursor)
return task_under_cursor or tasks[1]
end,
@@ -244,26 +258,6 @@ function Sidebar:toggle_preview()
util.scroll_to_end(winid)
end
-function Sidebar:change_task_detail(delta)
- local task = self:get_task_from_line()
- if not task then
- return
- end
- local detail = self.task_detail[task.id] or self.default_detail
- self.task_detail[task.id] = math.max(MIN_DETAIL, math.min(MAX_DETAIL, detail + delta))
- task_list.update(task)
-end
-
-function Sidebar:change_default_detail(delta)
- self.default_detail = math.max(MIN_DETAIL, math.min(MAX_DETAIL, self.default_detail + delta))
- for i, v in pairs(self.task_detail) do
- if (delta < 0 and v > self.default_detail) or (delta > 0 and v < self.default_detail) then
- self.task_detail[i] = nil
- end
- end
- task_list.update()
-end
-
---@private
---@return integer[]
function Sidebar:get_output_wins()
@@ -285,42 +279,31 @@ function Sidebar:highlight_focused()
return
end
- local start_lnum = 1
for _, v in ipairs(self.task_lines) do
- local end_lnum, task = v[1], v[2]
+ local start_lnum, end_lnum, task = v[1], v[2], v[3]
if task.id == self.focused_task_id then
- if vim.fn.has("nvim-0.10") == 1 then
- vim.api.nvim_buf_set_extmark(self.bufnr, ns, start_lnum - 1, 0, {
- line_hl_group = "CursorLine",
- end_row = end_lnum - 1,
- })
- else
- for i = start_lnum, end_lnum do
- vim.api.nvim_buf_add_highlight(self.bufnr, ns, "CursorLine", i - 1, 0, -1)
- end
- end
+ vim.api.nvim_buf_set_extmark(self.bufnr, ns, start_lnum - 1, 0, {
+ line_hl_group = "CursorLine",
+ end_row = end_lnum - 1,
+ })
end
- start_lnum = end_lnum + 2
end
end
function Sidebar:jump(direction)
local lnum = vim.api.nvim_win_get_cursor(0)[1]
- local prev = 1
- local cur = 1
- for _, v in ipairs(self.task_lines) do
- local end_lnum = v[1]
- local next = end_lnum + 2
- if end_lnum >= lnum then
- if direction < 0 then
- vim.api.nvim_win_set_cursor(0, { prev, 0 })
- else
- pcall(vim.api.nvim_win_set_cursor, 0, { next, 0 })
+ for i, v in ipairs(self.task_lines) do
+ local first_line = v[1]
+ if first_line <= lnum then
+ if direction < 0 and i > 1 then
+ local new_lnum = self.task_lines[i - 1][1]
+ vim.api.nvim_win_set_cursor(0, { new_lnum, 0 })
+ elseif direction > 0 and i < #self.task_lines then
+ local new_lnum = self.task_lines[i + 1][1]
+ vim.api.nvim_win_set_cursor(0, { new_lnum, 0 })
end
return
end
- prev = cur
- cur = next
end
end
@@ -344,7 +327,6 @@ function Sidebar:scroll_output(direction)
end
function Sidebar:run_action(name)
- vim.validate({ name = { name, "s", true } })
local task = self:get_task_from_line()
if not task then
return
@@ -353,97 +335,70 @@ function Sidebar:run_action(name)
action_util.run_task_action(task, name)
end
-function Sidebar:render(tasks)
+function Sidebar:toggle_show_wrapped()
+ self.list_task_opts.wrapped = not self.list_task_opts.wrapped
+ self:render()
+end
+
+function Sidebar:render()
if not vim.api.nvim_buf_is_valid(self.bufnr) then
return false
end
- local prev_num_lines = vim.api.nvim_buf_line_count(self.bufnr)
- local prev_first_task = self:get_task_from_line(1)
- local prev_first_task_id = prev_first_task and prev_first_task.id
+ local tasks = task_list.list_tasks(self.list_task_opts)
local ns = vim.api.nvim_create_namespace("overseer")
vim.api.nvim_buf_clear_namespace(self.bufnr, ns, 0, -1)
local lines = {}
- local highlights = {}
+ local extmarks = {}
self.task_lines = {}
- local subtask_prefix = "▐ "
+ -- virtual text lines for separating processes
+ local border = "OverseerTaskBorder"
+ local tl = config.task_list
+ local sep_lines = { { { tl.separator, border } } }
+ local child_indent = { tl.child_indent[1], border }
+ local child_sep_1 = { { { tl.child_indent[2], border }, { tl.separator, border } } }
+ local child_sep_2 = { { { tl.child_indent[3], border }, { tl.separator, border } } }
+
-- Iterate backwards so we show most recent tasks first
- for i = #tasks, 1, -1 do
- local task = tasks[i]
- local detail = self.task_detail[task.id] or self.default_detail
- local start_idx = #lines + 1
- local hl_start_idx = #highlights + 1
- task:render(lines, highlights, detail)
+ for i, task in ipairs(tasks) do
+ local line_start = #lines + 1
+ local task_lines = config.task_list.render(task)
-- Indent subtasks
if task.parent_id then
- for j = start_idx, #lines do
- lines[j] = subtask_prefix .. lines[j]
- end
- for j = hl_start_idx, #highlights do
- local hl = highlights[j]
- hl[3] = hl[3] + subtask_prefix:len()
- if hl[4] ~= -1 then
- hl[4] = hl[4] + subtask_prefix:len()
- end
- highlights[j] = hl
- end
- for j = start_idx, #lines do
- table.insert(highlights, { "OverseerTaskBorder", j, 0, subtask_prefix:len() })
+ for j = 1, #task_lines do
+ table.insert(task_lines[j], 1, child_indent)
end
end
- table.insert(self.task_lines, { #lines, task })
- if i > 1 then
- if tasks[i - 1].parent_id then
- table.insert(lines, subtask_prefix .. vim.fn.strcharpart(config.task_list.separator, 2))
+
+ vim.list_extend(lines, task_lines)
+ table.insert(self.task_lines, { line_start, #lines, task })
+
+ -- task separator
+ if i < #tasks then
+ local prev_is_child = i > 1 and tasks[i - 1].parent_id ~= nil
+ local next_is_child = tasks[i + 1].parent_id ~= nil
+ if next_is_child then
+ table.insert(extmarks, { #lines - 1, 0, { virt_lines = child_sep_1 } })
+ elseif prev_is_child then
+ table.insert(extmarks, { #lines - 1, 0, { virt_lines = child_sep_2 } })
else
- table.insert(lines, config.task_list.separator)
+ table.insert(extmarks, { #lines - 1, 0, { virt_lines = sep_lines } })
end
- table.insert(highlights, { "OverseerTaskBorder", #lines, 0, -1 })
end
end
- -- Attempting to render a newline within a line will cause a crash
- for i, line in ipairs(lines) do
- lines[i] = line:gsub("\n", " ")
+ util.render_buf_chunks(self.bufnr, ns, lines)
+ for _, extmark in ipairs(extmarks) do
+ vim.api.nvim_buf_set_extmark(self.bufnr, ns, extmark[1], extmark[2], extmark[3])
end
local sidebar_winid = self:get_winid()
- local view
- if sidebar_winid then
- vim.api.nvim_win_call(sidebar_winid, function()
- view = vim.fn.winsaveview()
- end)
- end
- vim.bo[self.bufnr].modifiable = true
- vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, true, lines)
- vim.bo[self.bufnr].modifiable = false
- vim.bo[self.bufnr].modified = false
- util.add_highlights(self.bufnr, ns, highlights)
-
if sidebar_winid then
- if view then
- vim.api.nvim_win_call(sidebar_winid, function()
- vim.fn.winrestview(view)
- end)
- end
-
- local new_first_task = tasks[#tasks]
- local new_first_task_id = new_first_task and new_first_task.id
- local new_line_count = vim.api.nvim_buf_line_count(self.bufnr)
-
- local in_sidebar = vim.api.nvim_get_current_win() == sidebar_winid
- local in_output_win = vim.b.overseer_task ~= nil
- if
- not in_sidebar
- and not in_output_win
- and prev_first_task_id ~= new_first_task_id
- and new_first_task_id
- then
- self:focus_task_id(new_first_task_id)
- elseif prev_num_lines ~= new_line_count then
- -- Make sure our cursor stays on the previously focused task, even if it's moved
- self:focus_task_id(self.focused_task_id)
+ local cursor = vim.api.nvim_win_get_cursor(sidebar_winid)
+ local task, offset = self:get_task_from_line(cursor[1])
+ if task then
+ self:focus_task_id(task.id, offset)
end
end
diff --git a/lua/overseer/task_view.lua b/lua/overseer/task_view.lua
index 53ad499d..0ffe8048 100644
--- a/lua/overseer/task_view.lua
+++ b/lua/overseer/task_view.lua
@@ -1,3 +1,4 @@
+local config = require("overseer.config")
local task_list = require("overseer.task_list")
local min_win_opts = {
@@ -18,21 +19,23 @@ local function set_minimal_win_opts(winid)
end
---@class (exact) overseer.TaskViewOpts
----@field select? fun(self: overseer.TaskView, tasks: overseer.Task[], task_under_cursor: overseer.Task?): overseer.Task?
+---@field select? fun(self: overseer.TaskView, tasks: overseer.Task[], task_under_cursor?: overseer.Task): nil|overseer.Task Select which task in the task list to display the output of
---@field close_on_list_close? boolean Close the window when the task list is closed
+---@field list_task_opts? overseer.ListTaskOpts Passed to list_tasks() to get the list of tasks to pass to the select() function
---@class overseer.TaskView
---@field winid integer
---@field private select fun(self: overseer.TaskView, tasks: overseer.Task[], task_under_cursor: overseer.Task?): overseer.Task?
---@field private autocmd_ids integer[]
+---@field private list_task_opts? overseer.ListTaskOpts
local TaskView = {}
----@param winid integer
+---@param winid? integer
---@param opts? overseer.TaskViewOpts
---@return overseer.TaskView
function TaskView.new(winid, opts)
opts = opts or {}
- if winid == 0 then
+ if not winid or winid == 0 then
winid = vim.api.nvim_get_current_win()
end
set_minimal_win_opts(winid)
@@ -41,6 +44,8 @@ function TaskView.new(winid, opts)
select = opts.select or function(self, tasks)
return tasks[1]
end,
+ list_task_opts = opts.list_task_opts
+ or { include_ephemeral = true, sort = config.task_list.sort },
autocmd_ids = {},
}
setmetatable(self, { __index = TaskView })
@@ -117,7 +122,7 @@ function TaskView.new(winid, opts)
end
---@return boolean
-function TaskView:is_disposed()
+function TaskView:is_win_closed()
return not vim.api.nvim_win_is_valid(self.winid)
end
@@ -134,7 +139,8 @@ local function get_empty_bufnr()
end
function TaskView:update()
- if self:is_disposed() then
+ if self:is_win_closed() then
+ self:dispose()
return
end
@@ -142,10 +148,10 @@ function TaskView:update()
TaskView.task_under_cursor = nil
end
- local tasks = task_list.list_tasks({ recent_first = true })
+ local tasks = task_list.list_tasks(self.list_task_opts)
local task = self.select(self, tasks, TaskView.task_under_cursor)
-- select() function can call dispose()
- if self:is_disposed() then
+ if self:is_win_closed() then
return
end
diff --git a/lua/overseer/template/init.lua b/lua/overseer/template.lua
similarity index 56%
rename from lua/overseer/template/init.lua
rename to lua/overseer/template.lua
index 2810f076..986f4287 100644
--- a/lua/overseer/template/init.lua
+++ b/lua/overseer/template.lua
@@ -1,3 +1,4 @@
+local Task = require("overseer.task")
local component = require("overseer.component")
local config = require("overseer.config")
local files = require("overseer.files")
@@ -5,47 +6,40 @@ local form = require("overseer.form")
local form_utils = require("overseer.form.utils")
local log = require("overseer.log")
local util = require("overseer.util")
----@diagnostic disable-next-line: deprecated
-local islist = vim.islist or vim.tbl_islist
local M = {}
----@class overseer.TemplateFileProvider
----@field module? string The name of the module this was loaded from
----@field condition? overseer.SearchCondition
+---@class (exact) overseer.TemplateFileProvider
+---@field condition? overseer.SearchCondition simple condition checks for when this provider is available
---@field cache_key? fun(opts: overseer.SearchParams): nil|string
----@field generator fun(opts: overseer.SearchParams, cb: fun(tmpls: overseer.TemplateDefinition[]))
+---@field generator fun(opts: overseer.SearchParams, cb: fun(tmpls_or_err: string|overseer.TemplateDefinition[])) : nil|string|overseer.TemplateDefinition[]
----@class overseer.TemplateProvider : overseer.TemplateFileProvider
+---@class (exact) overseer.TemplateProvider : overseer.TemplateFileProvider
---@field name string
-
----@class overseer.TemplateFileDefinition
---@field module? string The name of the module this was loaded from
----@field aliases? string[]
----@field desc? string
----@field tags? string[]
----@field params? overseer.Params|fun(): overseer.Params
----@field priority? number
----@field condition? overseer.SearchCondition
----@field builder fun(params: table): overseer.TaskDefinition
+
+---@class (exact) overseer.TemplateFileDefinition
+---@field aliases? string[] alternate names for the task. Used when running a task by name in overseer.run_task
+---@field desc? string extended description of the task
+---@field tags? string[] collection of tags used for optional task filtering
+---@field params? overseer.Params|fun(): overseer.Params parameters to customize the task. If any are missing when the task is built, overseer will prompt the user for input
+---@field condition? overseer.SearchCondition simple condition checks for when this task is available
+---@field builder fun(params: table): overseer.TaskDefinition function that builds the task definition from the parameters
---@field hide? boolean Hide from the template list
----@class overseer.TemplateDefinition : overseer.TemplateFileDefinition
+---@class (exact) overseer.TemplateDefinition : overseer.TemplateFileDefinition
---@field name string
+---@field module? string The name of the module this was loaded from
----@class overseer.SearchCondition
+---@class (exact) overseer.SearchCondition
---@field filetype? string|string[]
---@field dir? string|string[]
----@field callback? fun(search: overseer.SearchParams): boolean, nil|string
---@alias overseer.Params table
-local DEFAULT_PRIORITY = 50
-
----@type table
-local registry = {}
-
---@type overseer.TemplateProvider[]
-local providers = {}
+local registered_providers = {}
+---@type overseer.TemplateProvider[]
+local file_providers = {}
---@type table
local cached_provider_results = {}
@@ -55,6 +49,77 @@ local clear_cache_autocmd
local hooks = {}
+---@param defn overseer.TemplateProvider
+local function validate_template_provider(defn)
+ vim.validate("name", defn.name, "string")
+ vim.validate("generator", defn.generator, "function")
+ vim.validate("cache_key", defn.cache_key, "function", true)
+end
+
+---@param name string
+---@return nil|overseer.TemplateDefinition|overseer.TemplateProvider
+local function load_template(name)
+ local ok, defn = pcall(require, name)
+ if not ok then
+ log.error("Error loading template '%s': %s", name, defn)
+ return
+ end
+ name = name:gsub("^overseer%.template%.", "")
+ -- If this module was just a list of names, then it's an alias for a
+ -- collection of templates
+ if vim.islist(defn) then
+ log.warn("Deprecated: Template '%s' is a list of templates. We don't need this anymore.", name)
+ else
+ if not defn.name then
+ defn.name = name
+ end
+ defn.module = name
+ return defn
+ end
+end
+
+local _combined_providers
+local last_rtp
+---@return overseer.TemplateProvider[]
+local function get_providers()
+ if last_rtp == vim.o.runtimepath then
+ return _combined_providers
+ end
+ file_providers = {}
+ last_rtp = vim.o.runtimepath
+
+ local all_dirs = vim.list_extend({ "overseer/template" }, config.template_dirs)
+ for _, dir in ipairs(all_dirs) do
+ local path = vim.fs.joinpath("lua", dir, "**", "*.lua")
+ local task_files = vim.api.nvim_get_runtime_file(path, true)
+ for _, abspath in ipairs(task_files) do
+ local module_name = abspath:match("^.*(overseer/template/.*)%.lua$"):gsub("/", ".")
+ local tmpl = load_template(module_name)
+ if tmpl then
+ if tmpl.generator then
+ table.insert(file_providers, tmpl)
+ else
+ local provider = {
+ name = tmpl.module,
+ module = tmpl.module,
+ condition = tmpl.condition,
+ generator = function()
+ return { tmpl }
+ end,
+ }
+ ---@cast provider overseer.TemplateProvider
+ validate_template_provider(provider)
+ table.insert(file_providers, provider)
+ end
+ end
+ end
+ end
+
+ _combined_providers = vim.list_extend({}, registered_providers)
+ vim.list_extend(_combined_providers, file_providers)
+ return _combined_providers
+end
+
---@param condition? overseer.SearchCondition Template conditions
---@param tags? string[] Template tags
---@param search overseer.SearchParams Search parameters
@@ -64,6 +129,9 @@ local hooks = {}
local function condition_matches(condition, tags, search, match_tags)
condition = condition or {}
if condition.filetype then
+ if not search.filetype then
+ return false, string.format("Does not match filetype %s", vim.inspect(condition.filetype))
+ end
local search_fts = vim.split(search.filetype, ".", { plain = true })
local any_ft_match = false
for _, ft in util.iter_as_list(condition.filetype) do
@@ -104,12 +172,6 @@ local function condition_matches(condition, tags, search, match_tags)
end
end
- if condition.callback then
- local passed, message = condition.callback(search)
- if not passed then
- return false, message
- end
- end
return true
end
@@ -138,73 +200,13 @@ local function hook_matches(opts, search, name, module)
return true
end
----@param name string
-M.load_template = function(name)
- local ok, defn
- for _, template_dir in ipairs(config.template_dirs) do
- ok, defn = pcall(require, string.format("%s.%s", template_dir, name))
- if ok then
- break
- end
- end
- if not ok then
- log:error("Error loading template '%s': %s", name, defn)
- return
- end
- -- If this module was just a list of names, then it's an alias for a
- -- collection of templates
- if islist(defn) then
- for _, v in ipairs(defn) do
- M.load_template(v)
- end
- else
- if not defn.name then
- defn.name = name
- end
- defn.module = name
- local register_ok, err = pcall(M.register, defn)
- if not register_ok then
- log:error("Error loading template '%s': %s", name, err)
- end
- end
-end
-
-local initialized = false
-local function initialize()
- if initialized then
- return
- end
- for _, name in ipairs(config.templates) do
- M.load_template(name)
- end
- initialized = true
-end
-
----@param defn overseer.TemplateProvider
-local function validate_template_provider(defn)
- vim.validate({
- name = { defn.name, "s" },
- generator = { defn.generator, "f" },
- cache_key = { defn.cache_key, "f", true },
- })
- if not defn.cache_key then
- defn.cache_key = function(opts)
- return nil
- end
- end
-end
-
---@param defn overseer.TemplateDefinition
local function validate_template_definition(defn)
- defn.priority = defn.priority or DEFAULT_PRIORITY
defn.params = defn.params or {}
- vim.validate({
- name = { defn.name, "s" },
- desc = { defn.desc, "s", true },
- tags = { defn.tags, "t", true },
- priority = { defn.priority, "n" },
- builder = { defn.builder, "f" },
- })
+ vim.validate("name", defn.name, "string")
+ vim.validate("desc", defn.desc, "string", true)
+ vim.validate("tags", defn.tags, "table", true)
+ vim.validate("builder", defn.builder, "function")
local params = defn.params
if type(params) == "table" then
form_utils.validate_params(params)
@@ -246,8 +248,9 @@ end
---@param tmpl overseer.TemplateDefinition
---@param search overseer.SearchParams
---@param params table
+---@param on_build? fun(task_defn: overseer.TaskDefinition, util: overseer.TaskUtil)
---@return overseer.TaskDefinition
-local function build_task_args(tmpl, search, params)
+local function build_task_args(tmpl, search, params, on_build)
local task_defn = tmpl.builder(params)
task_defn.components = component.resolve(task_defn.components or { "default" })
@@ -257,12 +260,15 @@ local function build_task_args(tmpl, search, params)
end
end
+ if on_build then
+ on_build(task_defn, task_util)
+ end
return task_defn
end
---@class overseer.HookOptions : overseer.SearchCondition
----@field module? string
----@field name? string
+---@field module? string Only run if the template module matches this pattern (using string.match)
+---@field name? string Only run if the template name matches this pattern (using string.match)
---@param opts nil|overseer.HookOptions
---@param hook fun(task_defn: overseer.TaskDefinition, util: overseer.TaskUtil)
@@ -300,32 +306,39 @@ M.register = function(defn)
if defn.generator then
---@cast defn overseer.TemplateProvider
validate_template_provider(defn)
- table.insert(providers, defn)
+ table.insert(registered_providers, defn)
else
---@cast defn overseer.TemplateDefinition
validate_template_definition(defn)
- registry[defn.name] = defn
+ local provider = {
+ name = defn.name,
+ module = defn.module,
+ condition = defn.condition,
+ generator = function()
+ return { defn }
+ end,
+ }
+ ---@cast provider overseer.TemplateProvider
+ validate_template_provider(provider)
+ table.insert(registered_providers, provider)
end
end
---Check if template should prompt user for input. Exposed for testing
---@private
----@param prompt "always"|"never"|"allow"|"missing"|"avoid"
+---@param disallow_prompt? boolean
---@param param_schema table
---@param params table
---@return nil|boolean
---@return nil|string Error message if error is present
-M._should_prompt = function(prompt, param_schema, params)
+M._should_prompt = function(disallow_prompt, param_schema, params)
if vim.tbl_isempty(param_schema) then
return false
end
- local show_prompt = prompt == "always"
+ local show_prompt = false
for k, schema in pairs(param_schema) do
-- This parameter has no value passed in via the API
if params[k] == nil then
- if prompt == "missing" then
- show_prompt = true
- end
local has_default = schema.default ~= nil
if has_default then
-- Set the default value into the params, if any
@@ -333,16 +346,10 @@ M._should_prompt = function(prompt, param_schema, params)
end
-- If the param is not optional, process possible prompt values to show the prompt or error
- if not schema.optional then
- if prompt == "allow" then
- show_prompt = true
- end
- if not has_default then
- if prompt == "avoid" then
- show_prompt = true
- elseif prompt == "never" then
- return nil, string.format("Missing param %s", k)
- end
+ if not schema.optional and not has_default then
+ show_prompt = true
+ if disallow_prompt then
+ return nil, string.format("Missing param %s", k)
end
end
end
@@ -351,30 +358,27 @@ M._should_prompt = function(prompt, param_schema, params)
end
---@class overseer.TemplateBuildOpts
----@field prompt? "always"|"never"|"allow"|"missing"|"avoid"
---@field params table
---@field search overseer.SearchParams
+---@field disallow_prompt? boolean
+---@field on_build? fun(task_defn: overseer.TaskDefinition, util: overseer.TaskUtil)
---@param tmpl overseer.TemplateDefinition
---@param opts overseer.TemplateBuildOpts
----@param callback fun(task: overseer.TaskDefinition|nil, err: string|nil)
+---@param callback fun(err: string|nil, task: overseer.TaskDefinition|nil, params: table|nil)
M.build_task_args = function(tmpl, opts, callback)
- vim.validate({
- prompt = { opts.prompt, "s", true },
- params = { opts.params, "t" },
- })
- opts.prompt = opts.prompt or config.default_template_prompt
+ vim.validate("params", opts.params, "table")
local param_schema = tmpl.params or {}
if type(param_schema) == "function" then
param_schema = param_schema()
form_utils.validate_params(param_schema)
end
- local show_prompt, err = M._should_prompt(opts.prompt, param_schema, opts.params)
+ local show_prompt, err = M._should_prompt(opts.disallow_prompt, param_schema, opts.params)
if err then
- return callback(nil, err)
+ return callback(err)
end
if not show_prompt then
- callback(build_task_args(tmpl, opts.search, opts.params))
+ callback(nil, build_task_args(tmpl, opts.search, opts.params, opts.on_build), opts.params)
return
end
@@ -384,13 +388,51 @@ M.build_task_args = function(tmpl, opts, callback)
end
form.open(tmpl.name, schema, opts.params, function(final_params)
if final_params then
- callback(build_task_args(tmpl, opts.search, final_params))
+ callback(nil, build_task_args(tmpl, opts.search, final_params, opts.on_build), final_params)
else
callback()
end
end)
end
+---@class overseer.TaskBuildOpts : overseer.TemplateBuildOpts
+---@field cwd? string
+---@field env? table
+
+---@param tmpl overseer.TemplateDefinition
+---@param opts overseer.TaskBuildOpts
+---@param callback fun(err: string|nil, task: overseer.Task|nil)
+M.build_task = function(tmpl, opts, callback)
+ M.build_task_args(tmpl, opts, function(err, task_defn, params)
+ if err then
+ return callback(err)
+ end
+ if not task_defn then
+ return callback()
+ end
+ assert(task_defn)
+ assert(params)
+ local task
+ if opts.cwd then
+ task_defn.cwd = opts.cwd
+ end
+ if task_defn.env or opts.env then
+ task_defn.env = vim.tbl_deep_extend("force", task_defn.env or {}, opts.env or {})
+ end
+
+ ---@diagnostic disable-next-line: invisible
+ task_defn.from_template = {
+ name = tmpl.name,
+ env = opts.env,
+ params = params or {},
+ search = opts.search,
+ }
+
+ task = Task.new(task_defn)
+ callback(nil, task)
+ end)
+end
+
---@class overseer.SearchParams
---@field filetype? string
---@field tags? string[]
@@ -398,23 +440,32 @@ end
---@param opts overseer.SearchParams
M.clear_cache = function(opts)
- for _, provider in ipairs(providers) do
- local cache_key = provider.cache_key(opts)
- if cache_key then
- cached_provider_results[cache_key] = nil
+ for _, provider in ipairs(get_providers()) do
+ if provider.cache_key then
+ local cache_key = provider.cache_key(opts)
+ if cache_key then
+ cached_provider_results[cache_key] = nil
+ end
end
end
end
+---@class (exact) overseer.Report
+---@field providers table
+
+---@class (exact) overseer.ProviderReport
+---@field message? string
+---@field from_cache? boolean
+---@field total_tasks integer
+---@field available_tasks integer
+---@field elapsed_ms integer
+
---@param opts overseer.SearchParams
----@param cb fun(templates: overseer.TemplateDefinition[], report: table)
+---@param cb fun(templates: overseer.TemplateDefinition[], report: overseer.Report)
M.list = function(opts, cb)
- initialize()
- vim.validate({
- tags = { opts.tags, "t", true },
- dir = { opts.dir, "s" },
- filetype = { opts.filetype, "s", true },
- })
+ vim.validate("tags", opts.tags, "table", true)
+ vim.validate("dir", opts.dir, "string")
+ vim.validate("filetype", opts.filetype, "string", true)
-- Make sure the search dir is an absolute path
opts.dir = vim.fn.fnamemodify(opts.dir, ":p")
@@ -433,21 +484,10 @@ M.list = function(opts, cb)
end
local ret = {}
+ ---@type overseer.Report
local report = {
- templates = {},
providers = {},
}
- -- First add all of the simple templates that match the condition
- for _, tmpl in pairs(registry) do
- local is_match, message = condition_matches(tmpl.condition, tmpl.tags, opts, true)
- if is_match then
- table.insert(ret, tmpl)
- end
- report.templates[tmpl.name] = {
- is_present = is_match,
- message = message,
- }
- end
local finished_iterating = false
local pending = {}
@@ -456,14 +496,6 @@ M.list = function(opts, cb)
if not finished_iterating or not vim.tbl_isempty(pending) then
return
end
- -- Make sure results are sorted by priority, and then name
- table.sort(ret, function(a, b)
- if a.priority == b.priority then
- return a.name < b.name
- else
- return a.priority < b.priority
- end
- end)
cb(ret, report)
end
@@ -471,76 +503,101 @@ M.list = function(opts, cb)
local start_times = {}
local timed_out = false
---This is the async callback that is passed to generators
- ---@param tmpls overseer.TemplateDefinition[]
+ ---@param tmpls_or_err string|overseer.TemplateDefinition[]
---@param provider_name string
---@param module nil|string
---@param cache_key nil|string
---@param from_cache nil|boolean
- local function handle_tmpls(tmpls, provider_name, module, cache_key, from_cache)
- local elapsed_ms = (vim.loop.hrtime() - start_times[provider_name]) / 1e6
+ local function handle_tmpls(tmpls_or_err, provider_name, module, cache_key, from_cache)
+ local elapsed_ms = (vim.uv.now() - start_times[provider_name])
if
cache_key
- and config.template_cache_threshold > 0
- and elapsed_ms >= config.template_cache_threshold
+ and config.template_cache_threshold_ms > 0
+ and elapsed_ms >= config.template_cache_threshold_ms
+ and type(tmpls_or_err) == "table"
then
- log:debug("Caching %s: [%s] = %d", provider_name, cache_key, #tmpls)
- cached_provider_results[cache_key] = tmpls
+ log.debug("Caching %s: [%s] = %d", provider_name, cache_key, #tmpls_or_err)
+ cached_provider_results[cache_key] = tmpls_or_err
end
if not pending[provider_name] then
if not timed_out then
- log:warn("Template %s double-called callback", provider_name)
+ log.warn("Template %s double-called callback", provider_name)
end
return
end
pending[provider_name] = nil
local num_available = 0
- for _, tmpl in ipairs(tmpls) do
- -- Set the module on the template so it can be used to match hooks
- tmpl.module = module
- local ok, err = pcall(validate_template_definition, tmpl)
- if ok then
- if condition_matches(tmpl.condition, tmpl.tags, opts, true) then
- num_available = num_available + 1
- table.insert(ret, tmpl)
+
+ if type(tmpls_or_err) == "string" then
+ report.providers[provider_name] = {
+ message = tmpls_or_err,
+ from_cache = false,
+ total_tasks = 0,
+ available_tasks = 0,
+ elapsed_ms = elapsed_ms,
+ }
+ else
+ for _, tmpl in ipairs(tmpls_or_err) do
+ -- Set the module on the template so it can be used to match hooks
+ tmpl.module = module
+ local ok, err = pcall(validate_template_definition, tmpl)
+ if ok then
+ if condition_matches(tmpl.condition, tmpl.tags, opts, true) then
+ num_available = num_available + 1
+ table.insert(ret, tmpl)
+ end
+ else
+ log.error("Template %s from %s: %s", tmpl.name, provider_name, err)
end
- else
- log:error("Template %s from %s: %s", tmpl.name, provider_name, err)
end
+ report.providers[provider_name] = {
+ message = nil,
+ from_cache = from_cache,
+ total_tasks = #tmpls_or_err,
+ available_tasks = num_available,
+ elapsed_ms = elapsed_ms,
+ }
end
- report.providers[provider_name] = {
- is_present = true,
- message = nil,
- from_cache = from_cache,
- total_tasks = #tmpls,
- available_tasks = num_available,
- }
final_callback()
end
-- Timeout
- if config.template_timeout > 0 then
+ if config.template_timeout_ms > 0 then
vim.defer_fn(function()
if not vim.tbl_isempty(pending) then
timed_out = true
- log:error("Listing templates timed out. Pending providers: %s", vim.tbl_keys(pending))
+ log.error("Listing templates timed out. Pending providers: %s", vim.tbl_keys(pending))
pending = {}
final_callback()
-- Make sure that the callback doesn't get called again
cb = function() end
end
- end, config.template_timeout)
+ end, config.template_timeout_ms)
end
- for _, provider in ipairs(providers) do
+ for _, provider in ipairs(get_providers()) do
local provider_name = provider.name
local is_match, message = condition_matches(provider.condition, nil, opts, false)
if is_match then
- local cache_key = provider.cache_key(opts)
- local provider_cb = function(tmpls)
- handle_tmpls(tmpls, provider_name, provider.module, cache_key)
+ local cache_key
+ if provider.cache_key then
+ cache_key = provider.cache_key(opts)
end
- start_times[provider.name] = vim.loop.hrtime()
+ local provider_done = false
+ ---@param tmpls_or_err string|overseer.TemplateDefinition[]
+ local provider_cb = function(tmpls_or_err)
+ if provider_done then
+ log.error(
+ "Template provider %s: generator callback called twice. This can also happen if you return results from the function and call the callback.",
+ provider.name
+ )
+ else
+ provider_done = true
+ handle_tmpls(tmpls_or_err, provider_name, provider.module, cache_key)
+ end
+ end
+ start_times[provider.name] = vim.uv.now()
pending[provider.name] = true
if cache_key and cached_provider_results[cache_key] then
handle_tmpls(
@@ -555,19 +612,21 @@ M.list = function(opts, cb)
if ok then
if tmpls then
-- if there was a return value, the generator completed synchronously
- -- TODO deprecate this flow
provider_cb(tmpls)
end
else
- log:error("Template provider %s: %s", provider.name, tmpls)
+ assert(type(tmpls) == "string")
+ log.error("Template provider %s: %s", provider.name, tmpls)
+ local errmsg = vim.split(tmpls, "\n", { plain = true })[1]
+ provider_cb(errmsg)
end
end
else
report.providers[provider_name] = {
- is_present = is_match,
message = message,
total_tasks = 0,
available_tasks = 0,
+ elapsed_ms = 0,
}
end
end
@@ -579,12 +638,6 @@ end
---@param opts overseer.SearchParams
---@param cb fun(template: nil|overseer.TemplateDefinition)
M.get_by_name = function(name, opts, cb)
- initialize()
- local ret = registry[name]
- if ret and condition_matches(ret.condition, ret.tags, opts, false) then
- cb(ret)
- return
- end
M.list(opts, function(templates)
for _, tmpl in ipairs(templates) do
if tmpl.name == name or (tmpl.aliases and vim.tbl_contains(tmpl.aliases, name)) then
diff --git a/lua/overseer/template/builtin.lua b/lua/overseer/template/builtin.lua
deleted file mode 100644
index cf2cbc47..00000000
--- a/lua/overseer/template/builtin.lua
+++ /dev/null
@@ -1,16 +0,0 @@
-return {
- "cargo",
- "just",
- "make",
- "npm",
- "shell",
- "tox",
- "vscode",
- "mage",
- "mix",
- "deno",
- "rake",
- "task",
- "composer",
- "cargo-make",
-}
diff --git a/lua/overseer/template/cargo-make.lua b/lua/overseer/template/cargo-make.lua
index 59b2da70..98c27228 100644
--- a/lua/overseer/template/cargo-make.lua
+++ b/lua/overseer/template/cargo-make.lua
@@ -1,77 +1,39 @@
-local files = require("overseer.files")
-local log = require("overseer.log")
-local overseer = require("overseer")
-
----@type overseer.TemplateFileDefinition
-local tmpl = {
- priority = 60,
- params = {
- args = { type = "list", delimiter = " " },
- cwd = { optional = true },
- },
- builder = function(params)
- local cmd = { "cargo-make", "make" }
- return {
- args = params.args,
- cmd = cmd,
- cwd = params.cwd,
- }
- end,
-}
-
----@param opts overseer.SearchParams
----@return nil|string
-local function get_cargo_make_file(opts)
- return vim.fs.find("Makefile.toml", { upward = true, type = "file", path = opts.dir })[1]
-end
-
+---@type overseer.TemplateFileProvider
return {
- cache_key = function(opts)
- return get_cargo_make_file(opts)
- end,
- condition = {
- callback = function(opts)
- if vim.fn.executable("cargo-make") == 0 then
- return false, 'Command "cargo-make" not found'
- end
- if not get_cargo_make_file(opts) then
- return false, 'No "Makefile.toml" file found'
- end
- return true
- end,
- },
- generator = function(opts, cb)
- local ret = {}
-
- local cargo_make_file = get_cargo_make_file(opts)
+ generator = function(opts)
+ if vim.fn.executable("cargo-make") == 0 then
+ return 'Command "cargo-make" not found'
+ end
+ local cargo_make_file =
+ vim.fs.find("Makefile.toml", { upward = true, type = "file", path = opts.dir })[1]
if not cargo_make_file then
- log:error("No Makefile.toml file found")
- cb(ret)
- return
+ return 'No "Makefile.toml" file found'
end
+ local ret = {}
+
local cargo_make_file_dir = vim.fs.dirname(cargo_make_file)
- local data = files.read_file(cargo_make_file)
- if not data then
- log:error("Failed to read Makefile.toml file")
- cb(ret)
- return
+ local file = io.open(cargo_make_file, "r")
+ if not file then
+ return "Failed to read Makefile.toml file"
end
- for s in vim.gsplit(data, "\n", { plain = true }) do
+ for s in file:lines() do
local _, _, task_name = string.find(s, "^%[tasks%.(.+)%]$")
if task_name ~= nil then
- table.insert(
- ret,
- overseer.wrap_template(
- tmpl,
- { name = string.format("cargo-make %s", task_name) },
- { args = { task_name }, cwd = cargo_make_file_dir }
- )
- )
+ table.insert(ret, {
+ name = string.format("cargo-make %s", task_name),
+ builder = function()
+ return {
+ cmd = { "cargo-make", "make", task_name },
+ cwd = cargo_make_file_dir,
+ }
+ end,
+ })
end
end
+ file:close()
- cb(ret)
+ return ret
end,
}
diff --git a/lua/overseer/template/cargo.lua b/lua/overseer/template/cargo.lua
index 8e350235..c70bf61c 100644
--- a/lua/overseer/template/cargo.lua
+++ b/lua/overseer/template/cargo.lua
@@ -1,39 +1,8 @@
local constants = require("overseer.constants")
local json = require("overseer.json")
-local log = require("overseer.log")
local overseer = require("overseer")
local TAG = constants.TAG
----@type overseer.TemplateFileDefinition
-local tmpl = {
- priority = 60,
- params = {
- args = { type = "list", delimiter = " " },
- cwd = { optional = true },
- relative_file_root = {
- desc = "Relative filepaths will be joined to this root (instead of task cwd)",
- optional = true,
- },
- },
- builder = function(params)
- return {
- cmd = { "cargo" },
- args = params.args,
- cwd = params.cwd,
- default_component_params = {
- errorformat = [[%Eerror: %\%%(aborting %\|could not compile%\)%\@!%m,]]
- .. [[%Eerror[E%n]: %m,]]
- .. [[%Inote: %m,]]
- .. [[%Wwarning: %\%%(%.%# warning%\)%\@!%m,]]
- .. [[%C %#--> %f:%l:%c,]]
- .. [[%E left:%m,%C right:%m %f:%l:%c,%Z,]]
- .. [[%.%#panicked at \'%m\'\, %f:%l:%c]],
- relative_file_root = params.relative_file_root,
- },
- }
- end,
-}
-
---@param opts overseer.SearchParams
---@return nil|string
local function get_cargo_file(opts)
@@ -43,102 +12,89 @@ end
---@param cwd string
---@param cb fun(error: nil|string, workspace_root: nil|string)
local function get_workspace_root(cwd, cb)
- local jid = vim.fn.jobstart({ "cargo", "metadata", "--no-deps", "--format-version", "1" }, {
+ overseer.builtin.system({ "cargo", "metadata", "--no-deps", "--format-version", "1" }, {
cwd = cwd,
- stdout_buffered = true,
- on_stdout = function(j, output)
- local ok, data = pcall(json.decode, table.concat(output, ""))
- if ok then
- if data.workspace_root then
- cb(nil, data.workspace_root)
- else
- cb("No workspace_root found in output")
- end
+ text = true,
+ }, function(out)
+ local ok, data = pcall(json.decode, out.stdout)
+ if ok then
+ if data.workspace_root then
+ cb(nil, data.workspace_root)
else
- cb(data)
+ cb("No workspace_root found in output")
end
- end,
- })
- if jid == 0 then
- cb("Passed invalid arguments to 'cargo metadata'")
- elseif jid == -1 then
- cb("'cargo' is not executable")
- end
+ else
+ cb(data)
+ end
+ end)
end
+local commands = {
+ { args = { "build" }, tags = { TAG.BUILD } },
+ { args = { "run" }, tags = { TAG.RUN } },
+ { args = { "test" }, tags = { TAG.TEST } },
+ { args = { "clean" }, tags = { TAG.CLEAN } },
+ { args = { "check" } },
+ { args = { "doc" } },
+ { args = { "doc", "--open" } },
+ { args = { "bench" } },
+ { args = { "update" } },
+ { args = { "publish" } },
+ { args = { "clippy" } },
+ { args = { "fmt" } },
+}
+
return {
cache_key = function(opts)
return get_cargo_file(opts)
end,
- condition = {
- callback = function(opts)
- if vim.fn.executable("cargo") == 0 then
- return false, 'Command "cargo" not found'
- end
- if not get_cargo_file(opts) then
- return false, "No Cargo.toml file found"
- end
- return true
- end,
- },
generator = function(opts, cb)
- local cargo_dir = vim.fs.dirname(assert(get_cargo_file(opts)))
+ if vim.fn.executable("cargo") == 0 then
+ return 'Command "cargo" not found'
+ end
+ local cargo_file = get_cargo_file(opts)
+ if not cargo_file then
+ return "No Cargo.toml file found"
+ end
+ local cargo_dir = vim.fs.dirname(cargo_file)
local ret = {}
get_workspace_root(cargo_dir, function(err, workspace_root)
if err then
- log:error("Error fetching cargo workspace_root: %s", err)
- cb(ret)
- return
+ return cb(err)
end
- local commands = {
- { args = { "build" }, tags = { TAG.BUILD } },
- { args = { "run" }, tags = { TAG.RUN } },
- { args = { "test" }, tags = { TAG.TEST } },
- { args = { "clean" }, tags = { TAG.CLEAN } },
- { args = { "check" } },
- { args = { "doc" } },
- { args = { "doc", "--open" } },
- { args = { "bench" } },
- { args = { "update" } },
- { args = { "publish" } },
- { args = { "clippy" } },
- { args = { "fmt" } },
- }
- local roots =
- { {
- postfix = "",
- cwd = cargo_dir,
- priority = 55,
- } }
+ local roots = { {
+ postfix = "",
+ cwd = cargo_dir,
+ } }
if workspace_root ~= cargo_dir then
roots[1].relative_file_root = workspace_root
table.insert(roots, { postfix = " (workspace)", cwd = workspace_root })
end
for _, root in ipairs(roots) do
for _, command in ipairs(commands) do
- table.insert(
- ret,
- overseer.wrap_template(
- tmpl,
- {
- name = string.format("cargo %s%s", table.concat(command.args, " "), root.postfix),
- tags = command.tags,
- priority = root.priority,
- },
- { args = command.args, cwd = root.cwd, relative_file_root = root.relative_file_root }
- )
- )
+ table.insert(ret, {
+ name = string.format("cargo %s%s", table.concat(command.args, " "), root.postfix),
+ tags = command.tags,
+ builder = function()
+ return {
+ cmd = vim.list_extend({ "cargo" }, command.args),
+ cwd = root.cwd,
+ default_component_params = {
+ errorformat = [[%Eerror: %\%%(aborting %\|could not compile%\)%\@!%m,]]
+ .. [[%Eerror[E%n]: %m,]]
+ .. [[%Inote: %m,]]
+ .. [[%Wwarning: %\%%(%.%# warning%\)%\@!%m,]]
+ .. [[%C %#--> %f:%l:%c,]]
+ .. [[%E left:%m,%C right:%m %f:%l:%c,%Z,]]
+ .. [[%.%#panicked at \'%m\'\, %f:%l:%c]],
+ relative_file_root = root.relative_file_root,
+ },
+ }
+ end,
+ })
end
- table.insert(
- ret,
- overseer.wrap_template(
- tmpl,
- { name = "cargo" .. root.postfix },
- { cwd = root.cwd, relative_file_root = root.relative_file_root }
- )
- )
end
cb(ret)
end)
diff --git a/lua/overseer/template/composer.lua b/lua/overseer/template/composer.lua
index 4b238498..8f6caad4 100644
--- a/lua/overseer/template/composer.lua
+++ b/lua/overseer/template/composer.lua
@@ -1,60 +1,35 @@
local files = require("overseer.files")
-local overseer = require("overseer")
-
----@type overseer.TemplateFileDefinition
-local tmpl = {
- priority = 60,
- params = {
- args = { type = "list", delimiter = " " },
- cwd = { optional = true },
- },
- builder = function(params)
- local cmd = { "composer" }
- return {
- args = params.args,
- cmd = cmd,
- cwd = params.cwd,
- }
- end,
-}
-
-local function get_composer_file(opts)
- return vim.fs.find("composer.json", { upward = true, type = "file", path = opts.dir })[1]
-end
+---@type overseer.TemplateFileProvider
return {
- cache_key = function(opts)
- return get_composer_file(opts)
- end,
- condition = {
- callback = function(opts)
- if vim.fn.executable("composer") == 0 then
- return false, "executable composer not found"
- end
- if not get_composer_file(opts) then
- return false, "No composer.json file found"
- end
- return true
- end,
- },
- generator = function(opts, cb)
- local package = get_composer_file(opts)
+ generator = function(opts)
+ if vim.fn.executable("composer") == 0 then
+ return "executable composer not found"
+ end
+ local package =
+ vim.fs.find("composer.json", { upward = true, type = "file", path = opts.dir })[1]
+ if not package then
+ return "No composer.json file found"
+ end
local data = files.load_json_file(package)
local ret = {}
local scripts = data.scripts
- if scripts then
- for k in pairs(scripts) do
- table.insert(
- ret,
- overseer.wrap_template(
- tmpl,
- { name = string.format("composer %s", k) },
- { args = { "run-script", k } }
- )
- )
- end
+ if not scripts or vim.tbl_isempty(scripts) then
+ return "No scripts in composer.json"
+ end
+ local cwd = vim.fs.dirname(package)
+
+ for k in pairs(scripts) do
+ table.insert(ret, {
+ name = string.format("composer %s", k),
+ builder = function()
+ return {
+ cmd = { "composer", "run-script", k },
+ cwd = cwd,
+ }
+ end,
+ })
end
- table.insert(ret, overseer.wrap_template(tmpl, { name = "composer" }))
- cb(ret)
+ return ret
end,
}
diff --git a/lua/overseer/template/deno.lua b/lua/overseer/template/deno.lua
index 5da87b72..bb4cfbe7 100644
--- a/lua/overseer/template/deno.lua
+++ b/lua/overseer/template/deno.lua
@@ -1,62 +1,34 @@
local files = require("overseer.files")
-local overseer = require("overseer")
-
----@type overseer.TemplateFileDefinition
-local tmpl = {
- priority = 60,
- params = {
- args = { type = "list", delimiter = " " },
- cwd = { optional = true },
- },
- builder = function(params)
- local cmd = { "deno" }
- return {
- args = params.args,
- cmd = cmd,
- cwd = params.cwd,
- }
- end,
-}
-
-local function get_deno_file(opts)
- local deno_json = { "deno.json", "deno.jsonc" }
- return vim.fs.find(deno_json, { upward = true, type = "file", path = opts.dir })[1]
-end
+---@type overseer.TemplateFileProvider
return {
- cache_key = function(opts)
- return get_deno_file(opts)
- end,
- condition = {
- callback = function(opts)
- if vim.fn.executable("deno") == 0 then
- return false, "executable deno not found"
- end
- if not get_deno_file(opts) then
- return false, "No deno.{json,jsonc} file found"
- end
- return true
- end,
- },
- generator = function(opts, cb)
- local package = get_deno_file(opts)
+ generator = function(opts)
+ if vim.fn.executable("deno") == 0 then
+ return "executable deno not found"
+ end
+ local deno_json = { "deno.json", "deno.jsonc" }
+ local package = vim.fs.find(deno_json, { upward = true, type = "file", path = opts.dir })[1]
+ if not package then
+ return "No deno.{json,jsonc} file found"
+ end
local package_dir = vim.fs.dirname(package)
local data = files.load_json_file(package)
local ret = {}
local tasks = data.tasks
- if tasks then
- for k in pairs(tasks) do
- table.insert(
- ret,
- overseer.wrap_template(
- tmpl,
- { name = string.format("deno %s", k) },
- { args = { "task", k }, cwd = package_dir }
- )
- )
- end
+ if not tasks or vim.tbl_isempty(tasks) then
+ return "no tasks found in deno json file"
+ end
+ for k in pairs(tasks) do
+ table.insert(ret, {
+ name = string.format("deno %s", k),
+ builder = function()
+ return {
+ cmd = { "deno", "task", k },
+ cwd = package_dir,
+ }
+ end,
+ })
end
- table.insert(ret, overseer.wrap_template(tmpl, { name = "deno" }))
- cb(ret)
+ return ret
end,
}
diff --git a/lua/overseer/template/just.lua b/lua/overseer/template/just.lua
index b4502fb0..d44219b5 100644
--- a/lua/overseer/template/just.lua
+++ b/lua/overseer/template/just.lua
@@ -1,12 +1,16 @@
local log = require("overseer.log")
+local overseer = require("overseer")
+
+---@param name string
+---@return boolean
+local function is_justfile(name)
+ name = name:lower()
+ return name == "justfile" or name == ".justfile"
+end
---@param opts overseer.SearchParams
---@return nil|string
local function get_justfile(opts)
- local is_justfile = function(name)
- name = name:lower()
- return name == "justfile" or name == ".justfile"
- end
return vim.fs.find(is_justfile, { upward = true, path = opts.dir })[1]
end
@@ -15,87 +19,71 @@ local tmpl = {
cache_key = function(opts)
return get_justfile(opts)
end,
- condition = {
- callback = function(opts)
- if vim.fn.executable("just") == 0 then
- return false, 'Command "just" not found'
- end
- if not get_justfile(opts) then
- return false, "No justfile found"
- end
- return true
- end,
- },
generator = function(opts, cb)
+ if vim.fn.executable("just") == 0 then
+ return 'Command "just" not found'
+ end
+ local justfile = get_justfile(opts)
+ if not justfile then
+ return "No justfile found"
+ end
+ local cwd = vim.fs.dirname(justfile)
local ret = {}
- local jid = vim.fn.jobstart({ "just", "--unstable", "--dump", "--dump-format", "json" }, {
- cwd = opts.dir,
- stdout_buffered = true,
- on_stdout = vim.schedule_wrap(function(j, output)
- local ok, data =
- pcall(vim.json.decode, table.concat(output, ""), { luanil = { object = true } })
+ overseer.builtin.system(
+ { "just", "--unstable", "--dump", "--dump-format", "json" },
+ {
+ cwd = cwd,
+ text = true,
+ },
+ vim.schedule_wrap(function(out)
+ if out.code ~= 0 then
+ cb(out.stderr or out.stdout or "Error running 'just'")
+ return
+ end
+ local ok, data = pcall(vim.json.decode, out.stdout, { luanil = { object = true } })
if not ok then
- log:error("just produced invalid json: %s\n%s", data, output)
- cb(ret)
+ log.error("just produced invalid json: %s", out.stdout)
+ cb(string.format("just produced invalid json: %s\n%s", data))
return
end
assert(data)
- for k, recipe in pairs(data.recipes) do
- if recipe.private then
- goto continue
- end
- local params_defn = {}
- for _, param in ipairs(recipe.parameters) do
- local param_defn = {
- default = param.default,
- type = param.kind == "singular" and "string" or "list",
- delimiter = " ",
- }
- -- We don't want "star" arguments to be optional = true because then we won't show the
- -- input form. Instead, let's set a default value and filter it out in the builder.
- if param.kind == "star" and param.default == nil then
- if param_defn.type == "string" then
- param_defn.default = ""
- else
- param_defn.default = {}
- end
+ for _, recipe in pairs(data.recipes) do
+ if not recipe.private then
+ local params_defn = {}
+ for _, param in ipairs(recipe.parameters) do
+ params_defn[param.name] = {
+ default = param.default,
+ type = param.kind == "singular" and "string" or "list",
+ delimiter = " ",
+ }
end
- params_defn[param.name] = param_defn
- end
- table.insert(ret, {
- name = string.format("just %s", recipe.name),
- desc = recipe.doc,
- priority = k == data.first and 55 or 60,
- params = params_defn,
- builder = function(params)
- local cmd = { "just", recipe.name }
- for _, param in ipairs(recipe.parameters) do
- local v = params[param.name]
- if v then
- if type(v) == "table" then
- vim.list_extend(cmd, v)
- elseif v ~= "" then
- table.insert(cmd, v)
+
+ table.insert(ret, {
+ name = string.format("just %s", recipe.name),
+ desc = recipe.doc,
+ params = params_defn,
+ builder = function(params)
+ local cmd = { "just", recipe.name }
+ for _, param in ipairs(recipe.parameters) do
+ local v = params[param.name]
+ if v and v ~= "" then
+ if type(v) == "table" then
+ vim.list_extend(cmd, v)
+ else
+ table.insert(cmd, v)
+ end
end
end
- end
- return {
- cmd = cmd,
- }
- end,
- })
- ::continue::
+ return {
+ cmd = cmd,
+ }
+ end,
+ })
+ end
end
cb(ret)
- end),
- })
- if jid == 0 then
- log:error("Passed invalid arguments to 'just'")
- cb(ret)
- elseif jid == -1 then
- log:error("'just' is not executable")
- cb(ret)
- end
+ end)
+ )
end,
}
diff --git a/lua/overseer/template/mage.lua b/lua/overseer/template/mage.lua
index 4ef25037..a71d2eeb 100644
--- a/lua/overseer/template/mage.lua
+++ b/lua/overseer/template/mage.lua
@@ -1,9 +1,7 @@
+local overseer = require("overseer")
-- A make/rake-like build tool using Go
-- https://magefile.org/
-local log = require("overseer.log")
-local overseer = require("overseer")
-
---@param opts overseer.SearchParams
---@return nil|string
local function get_magefile(opts)
@@ -20,88 +18,53 @@ local function get_magedir(opts)
return vim.fs.find("magefiles", { upward = true, type = "directory", path = opts.dir })[1]
end
----@type overseer.TemplateDefinition
-local template = {
- name = "mage",
- priority = 60,
- params = {
- ---@type overseer.StringParam
- target = { optional = false, type = "string", desc = "target" },
- ---@type overseer.ListParam
- args = { optional = true, type = "list", delimiter = " " },
- },
- builder = function(params)
- local cmd = { "mage" }
- if params.target then
- table.insert(cmd, params.target)
- end
-
- ---@type overseer.TaskDefinition
- local task = { cmd = cmd }
-
- if params.args then
- task.args = params.args
- end
- return task
- end,
-}
-
---@type overseer.TemplateFileProvider
-local provider = {
+return {
cache_key = function(opts)
local magefile = get_magefile(opts)
return magefile ~= nil and magefile or get_magedir(opts)
end,
- condition = {
- callback = function(opts)
- if vim.fn.executable("mage") == 0 then
- return false, 'Command "mage" not found'
- end
- if not (get_magedir(opts) or get_magefile(opts)) then
- return false, "No magefile.go file or magefiles directory found"
- end
- return true
- end,
- },
generator = function(opts, cb)
- local ret = {}
+ if vim.fn.executable("mage") == 0 then
+ return 'Command "mage" not found'
+ end
local magefile, magedir = get_magefile(opts), get_magedir(opts)
- local jid = vim.fn.jobstart({
- "mage",
- "-l",
- }, {
- env = { MAGEFILE_ENABLE_COLOR = "false" },
- cwd = magefile ~= nil and vim.fs.dirname(magefile)
- or (magedir ~= nil and vim.fs.dirname(magedir) or opts.dir),
- stdout_buffered = true,
- on_stdout = vim.schedule_wrap(function(_, output)
- for _, line in ipairs(output) do
- if #line > 0 then
- local task_name, asterick, description = line:match("^ ([%w:]+)(%*?)%s+(.*)")
+ if not (magedir or magefile) then
+ return "No magefile.go file or magefiles directory found"
+ end
+ local cwd = magefile ~= nil and vim.fs.dirname(magefile)
+ or (magedir ~= nil and vim.fs.dirname(magedir) or opts.dir)
+ local ret = {}
+ overseer.builtin.system(
+ { "mage", "-l" },
+ {
+ env = { MAGEFILE_ENABLE_COLOR = "false" },
+ cwd = cwd,
+ text = true,
+ },
+ vim.schedule_wrap(function(out)
+ if out.code ~= 0 then
+ return cb(out.stderr or out.stdout or "Error running 'mage -l'")
+ end
+ for line in vim.gsplit(out.stdout, "\n") do
+ if line ~= "" then
+ local task_name, _, description = line:match("^ ([%w:]+)(%*?)%s+(.*)")
if task_name ~= nil then
- local override = {
+ table.insert(ret, {
name = string.format("mage %s", task_name),
desc = #description > 0 and description or nil,
- }
- -- default task
- if asterick == "*" then
- override.priority = 55
- end
- table.insert(ret, overseer.wrap_template(template, override, { target = task_name }))
+ builder = function()
+ return {
+ cmd = { "mage", task_name },
+ cwd = cwd,
+ }
+ end,
+ })
end
end
end
cb(ret)
- end),
- })
- if jid == 0 then
- log:error("Passed invalid arguments to 'mage'")
- cb(ret)
- elseif jid == -1 then
- log:error("'mage' is not executable")
- cb(ret)
- end
+ end)
+ )
end,
}
-
-return provider
diff --git a/lua/overseer/template/make.lua b/lua/overseer/template/make.lua
index b67bf3db..145e2211 100644
--- a/lua/overseer/template/make.lua
+++ b/lua/overseer/template/make.lua
@@ -1,69 +1,4 @@
-local constants = require("overseer.constants")
-local log = require("overseer.log")
local overseer = require("overseer")
-local TAG = constants.TAG
-
----@type overseer.TemplateFileDefinition
-local tmpl = {
- name = "make",
- priority = 60,
- tags = { TAG.BUILD },
- params = {
- args = { optional = true, type = "list", delimiter = " " },
- cwd = { optional = true },
- },
- builder = function(params)
- return {
- cmd = { "make" },
- args = params.args,
- cwd = params.cwd,
- }
- end,
-}
-
-local function parse_make_output(cwd, ret, cb)
- local jid = vim.fn.jobstart({ "make", "-rRpq" }, {
- cwd = cwd,
- stdout_buffered = true,
- env = {
- ["LANG"] = "C.UTF-8",
- },
- on_stdout = vim.schedule_wrap(function(j, output)
- local parsing = false
- local prev_line = ""
- for _, line in ipairs(output) do
- if line:find("# Files") == 1 then
- parsing = true
- elseif line:find("# Finished Make") == 1 then
- break
- elseif parsing then
- if line:match("^[^%.#%s]") and prev_line:find("# Not a target") ~= 1 then
- local idx = line:find(":")
- if idx then
- local target = line:sub(1, idx - 1)
- local override = { name = string.format("make %s", target) }
- table.insert(
- ret,
- overseer.wrap_template(tmpl, override, { args = { target }, cwd = cwd })
- )
- end
- end
- end
- prev_line = line
- end
-
- cb(ret)
- end),
- })
- if jid == 0 then
- log:error("Passed invalid arguments to 'make'")
- cb(ret)
- elseif jid == -1 then
- log:error("'make' is not executable")
- cb(ret)
- end
-end
-
---@param opts overseer.SearchParams
---@return nil|string
local function get_makefile(opts)
@@ -71,27 +6,64 @@ local function get_makefile(opts)
end
---@type overseer.TemplateFileProvider
-local provider = {
+return {
cache_key = function(opts)
return get_makefile(opts)
end,
- condition = {
- callback = function(opts)
- if vim.fn.executable("make") == 0 then
- return false, 'Command "make" not found'
- end
- if not get_makefile(opts) then
- return false, "No Makefile found"
- end
- return true
- end,
- },
generator = function(opts, cb)
- local makefile = assert(get_makefile(opts))
+ if vim.fn.executable("make") == 0 then
+ return 'Command "make" not found'
+ end
+ local makefile = get_makefile(opts)
+ if not makefile then
+ return "No Makefile found"
+ end
local cwd = vim.fs.dirname(makefile)
- local ret = { overseer.wrap_template(tmpl, nil, { cwd = cwd }) }
- parse_make_output(cwd, ret, cb)
+ local ret = {}
+ overseer.builtin.system(
+ { "make", "-rRpq" },
+ {
+ cwd = cwd,
+ text = true,
+ env = {
+ ["LANG"] = "C.UTF-8",
+ },
+ },
+ vim.schedule_wrap(function(out)
+ if out.code ~= 0 and out.code ~= 1 then
+ return cb(out.stderr or out.stdout or "Error running 'make'")
+ end
+
+ local parsing = false
+ local prev_line = ""
+ for line in vim.gsplit(out.stdout, "\n") do
+ if line:find("# Files") == 1 then
+ parsing = true
+ elseif line:find("# Finished Make") == 1 then
+ break
+ elseif parsing then
+ if line:match("^[^%.#%s]") and prev_line:find("# Not a target") ~= 1 then
+ local idx = line:find(":")
+ if idx then
+ local target = line:sub(1, idx - 1)
+ table.insert(ret, {
+ name = string.format("make %s", target),
+ builder = function()
+ return {
+ cmd = { "make", target },
+ cwd = cwd,
+ }
+ end,
+ })
+ end
+ end
+ end
+ prev_line = line
+ end
+
+ cb(ret)
+ end)
+ )
end,
}
-return provider
diff --git a/lua/overseer/template/mix.lua b/lua/overseer/template/mix.lua
index 9284473f..b095b9f7 100644
--- a/lua/overseer/template/mix.lua
+++ b/lua/overseer/template/mix.lua
@@ -1,78 +1,45 @@
-local log = require("overseer.log")
local overseer = require("overseer")
-
----@type overseer.TemplateFileDefinition
-local tmpl = {
- priority = 60,
- params = {
- subcmd = { optional = true },
- args = { type = "list", delimiter = " ", default = {} },
- },
- builder = function(params)
- local cmd = { "mix" }
- local args = params.args or {}
- if params.subcmd then
- table.insert(args, 1, params.subcmd)
- end
- return {
- cmd = cmd,
- args = args,
- }
- end,
-}
-
---@param opts overseer.SearchParams
---@return nil|string
local function get_mix_file(opts)
return vim.fs.find("mix.exs", { upward = true, type = "file", path = opts.dir })[1]
end
+---@type overseer.TemplateFileProvider
return {
cache_key = function(opts)
return get_mix_file(opts)
end,
- condition = {
- callback = function(opts)
- if not get_mix_file(opts) then
- return false, "No mix.exs file found"
- end
- return true
- end,
- },
generator = function(opts, cb)
-- mix will not return all the tasks unless you invoke it in the mix.exs folder
- local mix_folder = vim.fs.dirname(assert(get_mix_file(opts)))
+ local mix_file = get_mix_file(opts)
+ if not mix_file then
+ return "No mix.exs file found"
+ end
+ local mix_folder = vim.fs.dirname(mix_file)
local ret = {}
- local jid = vim.fn.jobstart({
- "mix",
- "help",
- }, {
- cwd = mix_folder,
- stdout_buffered = true,
- on_stdout = vim.schedule_wrap(function(j, output)
- for _, line in ipairs(output) do
+ overseer.builtin.system(
+ { "mix", "help" },
+ {
+ cwd = mix_folder,
+ text = true,
+ },
+ vim.schedule_wrap(function(out)
+ if out.code ~= 0 then
+ return cb(out.stderr or out.stdout or "Error running 'mix help'")
+ end
+ for line in vim.gsplit(out.stdout, "\n") do
local task_name = line:match("mix (%S+)%s")
- table.insert(
- ret,
- overseer.wrap_template(
- tmpl,
- { name = string.format("mix %s", task_name) },
- { subcmd = task_name }
- )
- )
+ table.insert(ret, {
+ name = string.format("mix %s", task_name),
+ builder = function()
+ return {
+ cmd = { "mix", task_name },
+ }
+ end,
+ })
end
- table.insert(ret, overseer.wrap_template(tmpl, { name = "mix", priority = 65 }))
- end),
- on_exit = vim.schedule_wrap(function(j, output)
- cb(ret)
- end),
- })
- if jid == 0 then
- log:error("Passed invalid arguments to 'mix'")
- cb(ret)
- elseif jid == -1 then
- log:error("'mix' is not executable")
- cb(ret)
- end
+ end)
+ )
end,
}
diff --git a/lua/overseer/template/npm.lua b/lua/overseer/template/npm.lua
index 4deec256..50fab99a 100644
--- a/lua/overseer/template/npm.lua
+++ b/lua/overseer/template/npm.lua
@@ -1,6 +1,4 @@
local files = require("overseer.files")
-local overseer = require("overseer")
-local util = require("overseer.util")
---@type table
local mgr_lockfiles = {
@@ -10,23 +8,6 @@ local mgr_lockfiles = {
bun = { "bun.lockb", "bun.lock" },
}
----@type overseer.TemplateFileDefinition
-local tmpl = {
- priority = 60,
- params = {
- args = { optional = true, type = "list", delimiter = " " },
- cwd = { optional = true },
- bin = { optional = true, type = "string" },
- },
- builder = function(params)
- return {
- cmd = { params.bin },
- args = params.args,
- cwd = params.cwd,
- }
- end,
-}
-
---@param opts overseer.SearchParams
local function get_candidate_package_files(opts)
-- Some projects have package.json files in subfolders, which are not the main project package.json file,
@@ -71,77 +52,65 @@ end
local function pick_package_manager(package_file)
local package_dir = vim.fs.dirname(package_file)
for mgr, lockfiles in pairs(mgr_lockfiles) do
- if
- util.list_any(lockfiles, function(lockfile)
- return files.exists(files.join(package_dir, lockfile))
- end)
- then
- return mgr
+ for _, lockfile in ipairs(lockfiles) do
+ if vim.uv.fs_stat(vim.fs.joinpath(package_dir, lockfile)) then
+ return mgr
+ end
end
end
return "npm"
end
+---@type overseer.TemplateFileProvider
return {
- cache_key = function(opts)
- return get_package_file(opts)
- end,
- condition = {
- callback = function(opts)
- local package_file = get_package_file(opts)
- if not package_file then
- return false, "No package.json file found"
- end
- local package_manager = pick_package_manager(package_file)
- if vim.fn.executable(package_manager) == 0 then
- return false, string.format("Could not find command '%s'", package_manager)
- end
- return true
- end,
- },
- generator = function(opts, cb)
+ generator = function(opts)
local package = get_package_file(opts)
if not package then
- cb({})
- return
+ return "No package.json file found"
end
local bin = pick_package_manager(package)
+ if vim.fn.executable(bin) == 0 then
+ return string.format("Could not find command '%s'", bin)
+ end
+
local data = files.load_json_file(package)
local ret = {}
+ local cwd = vim.fs.dirname(package)
if data.scripts then
for k in pairs(data.scripts) do
- table.insert(
- ret,
- overseer.wrap_template(
- tmpl,
- { name = string.format("%s %s", bin, k) },
- { args = { "run", k }, bin = bin, cwd = vim.fs.dirname(package) }
- )
- )
+ table.insert(ret, {
+ name = string.format("%s %s", bin, k),
+ builder = function()
+ return {
+ cmd = { bin, "run", k },
+ cwd = cwd,
+ }
+ end,
+ })
end
end
-- Load tasks from workspaces
if data.workspaces then
for _, workspace in ipairs(data.workspaces) do
- local workspace_path = files.join(vim.fs.dirname(package), workspace)
- local workspace_package_file = files.join(workspace_path, "package.json")
+ local workspace_path = vim.fs.joinpath(cwd, workspace)
+ local workspace_package_file = vim.fs.joinpath(workspace_path, "package.json")
local workspace_data = files.load_json_file(workspace_package_file)
if workspace_data and workspace_data.scripts then
for k in pairs(workspace_data.scripts) do
- table.insert(
- ret,
- overseer.wrap_template(
- tmpl,
- { name = string.format("%s[%s] %s", bin, workspace, k) },
- { args = { "run", k }, bin = bin, cwd = workspace_path }
- )
- )
+ table.insert(ret, {
+ name = string.format("%s[%s] %s", bin, workspace, k),
+ builder = function()
+ return {
+ cmd = { bin, "run", k },
+ cwd = workspace_path,
+ }
+ end,
+ })
end
end
end
end
- table.insert(ret, overseer.wrap_template(tmpl, { name = bin }, { bin = bin }))
- cb(ret)
+ return ret
end,
}
diff --git a/lua/overseer/template/rake.lua b/lua/overseer/template/rake.lua
index cf594dd2..089a00ee 100644
--- a/lua/overseer/template/rake.lua
+++ b/lua/overseer/template/rake.lua
@@ -1,4 +1,4 @@
-local log = require("overseer.log")
+local overseer = require("overseer")
---@param opts overseer.SearchParams
---@return nil|string
@@ -7,33 +7,33 @@ local function get_rakefile(opts)
end
---@type overseer.TemplateFileProvider
-local provider = {
+return {
cache_key = function(opts)
return get_rakefile(opts)
end,
- condition = {
- callback = function(opts)
- if vim.fn.executable("rake") == 0 then
- return false, 'Command "rake" not found'
- end
- if not get_rakefile(opts) then
- return false, "No Rakefile found"
- end
- return true
- end,
- },
generator = function(opts, cb)
+ if vim.fn.executable("rake") == 0 then
+ return 'Command "rake" not found'
+ end
+ local rakefile = get_rakefile(opts)
+ if not rakefile then
+ return "No Rakefile found"
+ end
+ local cwd = vim.fs.dirname(rakefile)
local ret = {}
- local jid = vim.fn.jobstart({
- "rake",
- "-T",
- }, {
- cwd = opts.dir,
- stdout_buffered = true,
- on_stdout = vim.schedule_wrap(function(j, output)
+ overseer.builtin.system(
+ { "rake", "-T" },
+ {
+ cwd = cwd,
+ text = true,
+ },
+ vim.schedule_wrap(function(out)
+ if out.code ~= 0 then
+ return cb(out.stderr or out.stdout or "Error running 'rake -T'")
+ end
local tasks = {}
- for _, line in ipairs(output) do
- if #line > 0 then
+ for line in vim.gsplit(out.stdout, "\n") do
+ if line ~= "" then
local task_name, params = line:match("^rake (%S+)(%[%S+%])")
if task_name == nil then
-- no parameters
@@ -57,7 +57,6 @@ local provider = {
for _, task in ipairs(tasks) do
table.insert(ret, {
name = string.format("rake %s", task.task_name),
- priority = 60,
params = task.args,
builder = function(parms)
local param_vals = {}
@@ -68,26 +67,15 @@ local provider = {
if #param_vals > 0 then
p = "[" .. table.concat(param_vals, ",") .. "]"
end
- local cmd = { "rake", task.task_name .. p }
return {
- cmd = cmd,
+ cmd = { "rake", task.task_name .. p },
}
end,
})
end
- end),
- on_exit = vim.schedule_wrap(function(j, output)
+
cb(ret)
- end),
- })
- if jid == 0 then
- log:error("Passed invalid arguments to 'rake'")
- cb(ret)
- elseif jid == -1 then
- log:error("'rake' is not executable")
- cb(ret)
- end
+ end)
+ )
end,
}
-
-return provider
diff --git a/lua/overseer/template/shell.lua b/lua/overseer/template/shell.lua
deleted file mode 100644
index 63727ec4..00000000
--- a/lua/overseer/template/shell.lua
+++ /dev/null
@@ -1,34 +0,0 @@
----@type overseer.TemplateFileDefinition
-local tmpl = {
- name = "shell",
- params = {
- cmd = { type = "string", order = 1 },
- name = { type = "string", optional = true, order = 2 },
- cwd = { type = "string", optional = true, order = 4 },
- env = { type = "opaque", optional = true },
- metadata = { type = "opaque", optional = true },
- components = { type = "opaque", optional = true },
- strategy = { type = "opaque", optional = true },
- expand_cmd = {
- desc = "Run expandcmd() on command before execution",
- type = "boolean",
- default = true,
- optional = true,
- order = 3,
- },
- },
- builder = function(params)
- local cmd = params.expand_cmd and vim.fn.expandcmd(params.cmd) or params.cmd
- return {
- cmd = cmd,
- env = params.env,
- cwd = params.cwd,
- name = params.name,
- metadata = params.metadata,
- components = params.components,
- strategy = params.strategy,
- }
- end,
-}
-
-return tmpl
diff --git a/lua/overseer/template/task.lua b/lua/overseer/template/task.lua
index 7eddb6b5..4f0a8405 100644
--- a/lua/overseer/template/task.lua
+++ b/lua/overseer/template/task.lua
@@ -1,6 +1,5 @@
-- A task runner / simpler Make alternative written in Go
-- https://taskfile.dev/
-
local log = require("overseer.log")
local overseer = require("overseer")
@@ -17,94 +16,51 @@ local function find_taskfile(opts)
return vim.fs.find(taskfiles, { upward = true, type = "file", path = opts.dir })[1]
end
----@type overseer.TemplateDefinition
-local template = {
- name = "task",
- desc = "default target",
- priority = 60,
- params = {
- ---@type overseer.StringParam
- target = { optional = false, type = "string", desc = "target" },
- ---@type overseer.ListParam
- args = { optional = true, type = "list", delimiter = " " },
- ---@type overseer.StringParam
- cwd = { optional = true },
- },
- builder = function(params)
- local cmd = { "task" }
- if params.target then
- table.insert(cmd, params.target)
- end
-
- ---@type overseer.TaskDefinition
- local task = {
- cmd = cmd,
- cwd = params.cwd,
- }
-
- if params.args then
- task.args = vim.list_extend({ "--" }, params.args)
- end
- return task
- end,
-}
-
----@type overseer.TemplateProvider
-local provider = {
- name = "task",
+---@type overseer.TemplateFileProvider
+return {
cache_key = function(opts)
return find_taskfile(opts)
end,
- condition = {
- callback = function(opts)
- if vim.fn.executable("task") == 0 then
- return false, 'Command "task" not found'
- end
- if not find_taskfile(opts) then
- return false, "No Taskfile found"
- end
- return true
- end,
- },
generator = function(opts, cb)
+ if vim.fn.executable("task") == 0 then
+ return 'Command "task" not found'
+ end
+ local taskfile = find_taskfile(opts)
+ if not taskfile then
+ return "No Taskfile found"
+ end
+ local cwd = vim.fs.dirname(taskfile)
local ret = {}
- local cmd = { "task", "--list-all", "--json" }
- local jid = vim.fn.jobstart(cmd, {
- cwd = opts.dir,
- stdout_buffered = true,
- on_stdout = vim.schedule_wrap(function(_, output)
- local ok, data =
- pcall(vim.json.decode, table.concat(output, "\n"), { luanil = { object = true } })
+ overseer.builtin.system(
+ { "task", "--list-all", "--json" },
+ {
+ cwd = cwd,
+ text = true,
+ },
+ vim.schedule_wrap(function(out)
+ if out.code ~= 0 then
+ return cb(out.stderr or out.stdout or "Error running 'task'")
+ end
+ local ok, data = pcall(vim.json.decode, out.stdout, { luanil = { object = true } })
if not ok then
- log:error("Task produced invalid json: %s\n%s", data, output)
- cb(ret)
- return
+ log.error("Task produced invalid json: %s", out.stdout)
+ return cb(data)
end
assert(data)
for _, target in ipairs(data.tasks) do
- local override = {
+ table.insert(ret, {
name = string.format("task %s", target.name),
desc = target.desc,
- }
- if target.name == "default" then
- override.priority = 55
- end
- table.insert(
- ret,
- overseer.wrap_template(template, override, { target = target.name, cwd = opts.dir })
- )
+ builder = function()
+ return {
+ cmd = { "task", target },
+ cwd = cwd,
+ }
+ end,
+ })
end
cb(ret)
- end),
- })
- if jid == 0 then
- log:error("Passed invalid arguments to 'task'")
- cb(ret)
- elseif jid == -1 then
- log:error("'task' is not executable")
- cb(ret)
- end
+ end)
+ )
end,
}
-
-return provider
diff --git a/lua/overseer/template/tox.lua b/lua/overseer/template/tox.lua
index 4d6a551f..1280d375 100644
--- a/lua/overseer/template/tox.lua
+++ b/lua/overseer/template/tox.lua
@@ -1,47 +1,16 @@
-local files = require("overseer.files")
-local overseer = require("overseer")
-
----@type overseer.TemplateDefinition
-local tmpl = {
- name = "tox",
- priority = 60,
- params = {
- args = { optional = true, type = "list", delimiter = " " },
- },
- builder = function(params)
- local cmd = { "tox" }
- if params.args then
- cmd = vim.list_extend(cmd, params.args)
- end
- return {
- cmd = cmd,
- }
- end,
-}
-
----@param opts overseer.SearchParams
----@return nil|string
-local function get_toxfile(opts)
- return vim.fs.find("tox.ini", { upward = true, type = "file", path = opts.dir })[1]
-end
-
+---@type overseer.TemplateFileProvider
return {
- cache_key = function(opts)
- return get_toxfile(opts)
- end,
- condition = {
- callback = function(opts)
- if not get_toxfile(opts) then
- return false, "No tox.ini file found"
- end
- return true
- end,
- },
- generator = function(opts, cb)
- local tox_file = assert(get_toxfile(opts))
- local content = assert(files.read_file(tox_file))
+ generator = function(opts)
+ local tox_file = vim.fs.find("tox.ini", { upward = true, type = "file", path = opts.dir })[1]
+ if not tox_file then
+ return "No tox.ini file found"
+ end
+ local file = io.open(tox_file, "r")
+ if not file then
+ return "Failed to read tox.ini file"
+ end
local targets = {}
- for line in vim.gsplit(content, "\n") do
+ for line in file:lines() do
local envlist = line:match("^envlist%s*=%s*(.+)$")
if envlist then
for t in vim.gsplit(envlist, "%s*,%s*") do
@@ -57,17 +26,19 @@ return {
end
end
- local ret = { tmpl }
+ local ret = {}
+ local cwd = vim.fs.dirname(tox_file)
for k in pairs(targets) do
- table.insert(
- ret,
- overseer.wrap_template(
- tmpl,
- { name = string.format("tox -e %s", k) },
- { args = { "-e", k } }
- )
- )
+ table.insert(ret, {
+ name = string.format("tox %s", k),
+ builder = function()
+ return {
+ cmd = { "tox", "-e", k },
+ cwd = cwd,
+ }
+ end,
+ })
end
- cb(ret)
+ return ret
end,
}
diff --git a/lua/overseer/template/vscode.lua b/lua/overseer/template/vscode.lua
new file mode 100644
index 00000000..8b87087f
--- /dev/null
+++ b/lua/overseer/template/vscode.lua
@@ -0,0 +1,48 @@
+local files = require("overseer.files")
+local variables = require("overseer.vscode.variables")
+local vs_util = require("overseer.vscode.vs_util")
+local vscode = require("overseer.vscode")
+
+---@type overseer.TemplateFileProvider
+return {
+ generator = function(opts)
+ local tasks_file = vs_util.get_tasks_file(vim.fn.getcwd(), opts.dir)
+ if not tasks_file then
+ return "No .vscode/tasks.json file found"
+ end
+ local content = vs_util.load_tasks_file(tasks_file)
+ local global_defaults = {}
+ for k, v in pairs(content) do
+ if k ~= "version" and k ~= "tasks" then
+ global_defaults[k] = v
+ end
+ end
+ local os_key
+ if files.is_windows then
+ os_key = "windows"
+ elseif files.is_mac then
+ os_key = "osx"
+ else
+ os_key = "linux"
+ end
+ if content[os_key] then
+ global_defaults = vim.tbl_deep_extend("force", global_defaults, content[os_key])
+ end
+ local ret = {}
+ local precalculated_vars = variables.precalculate_vars()
+
+ if content.tasks == nil then
+ return "No 'tasks' key found in '.vscode/tasks.json'"
+ end
+
+ for _, task in ipairs(content.tasks) do
+ local defn = vim.tbl_deep_extend("force", global_defaults, task)
+ defn = vim.tbl_deep_extend("force", defn, task[os_key] or {})
+ local tmpl = vscode.convert_vscode_task(defn, precalculated_vars)
+ if tmpl then
+ table.insert(ret, tmpl)
+ end
+ end
+ return ret
+ end,
+}
diff --git a/lua/overseer/types.lua b/lua/overseer/types.lua
deleted file mode 100644
index 5fb5bf5d..00000000
--- a/lua/overseer/types.lua
+++ /dev/null
@@ -1,52 +0,0 @@
----@class (exact) overseer.Config
----@field strategy? overseer.Serialized Default task strategy
----@field templates? string[] Template modules to load
----@field auto_detect_success_color? boolean
----@field dap? boolean Patch nvim-dap to support preLaunchTask and postDebugTask
----@field task_list? overseer.ConfigTaskList Configure the task list
----@field actions? any See :help overseer-actions
----@field form? overseer.ConfigFloatWin Configure the floating window used for task templates that require input and the floating window used for editing tasks
----@field task_launcher? table
----@field task_editor? table
----@field confirm? overseer.ConfigFloatWin
----@field task_win? overseer.ConfigTaskWin
----@field help_win? overseer.ConfigFloatWin
----@field component_aliases? table Aliases for bundles of components. Redefine the builtins, or create your own.
----@field bundles? overseer.ConfigBundles
----@field preload_components? string[] A list of components to preload on setup. Only matters if you want them to show up in the task editor.
----@field default_template_prompt? "always"|"missing"|"allow"|"avoid"|"never" Controls when the parameter prompt is shown when running a template
----@field template_timeout? integer For template providers, how long to wait (in ms) before timing out. Set to 0 to disable timeouts.
----@field template_cache_threshold? integer Cache template provider results if the provider takes longer than this to run. Time is in ms. Set to 0 to disable caching.
----@field log? table[]
-
----@class (exact) overseer.ConfigTaskList
----@field default_detail? 1|2|3 Default detail level for tasks. Can be 1-3.
----@field max_width? number|number[] Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). min_width and max_width can be a single value or a list of mixed integer/float types. max_width = {100, 0.2} means "the lesser of 100 columns or 20% of total"
----@field min_width? number|number[] min_width = {40, 0.1} means "the greater of 40 columns or 10% of total"
----@field width? number optionally define an integer/float for the exact width of the task list
----@field max_height? number|number[]
----@field min_height? number|number[]
----@field height? number
----@field separator? string String that separates tasks
----@field direction? string Default direction. Can be "left", "right", or "bottom"
----@field bindings? table Set keymap to false to remove default behavior
-
----@class (exact) overseer.ConfigFloatWin
----@field border? string|table
----@field zindex? integer
----@field min_width? number|number[]
----@field max_width? number|number[]
----@field min_height? number|number[]
----@field max_height? number|number[]
----@field win_opts? table
-
----@class (exact) overseer.ConfigTaskWin
----@field border? string|table
----@field padding? integer
----@field win_opts? table
-
----@class (exact) overseer.ConfigBundles
----@field save_task_opts? table When saving a bundle with OverseerSaveBundle or save_task_bundle(), filter the tasks with these options (passed to list_tasks())
----@field autostart_on_load? boolean Autostart tasks when they are loaded from a bundle
-
----@alias overseer.Serialized string|{[1]: string, [string]: any}
diff --git a/lua/overseer/util.lua b/lua/overseer/util.lua
index 15d3b983..c40670f3 100644
--- a/lua/overseer/util.lua
+++ b/lua/overseer/util.lua
@@ -1,3 +1,4 @@
+local log = require("overseer.log")
local M = {}
---@param winid? number
@@ -147,13 +148,38 @@ M.scroll_to_end = function(winid)
vim.api.nvim_set_option_value("scrolloff", scrolloff, { scope = "local", win = winid })
end
----@param bufnr number
----@param ns number
----@param highlights table
-M.add_highlights = function(bufnr, ns, highlights)
- for _, hl in ipairs(highlights) do
- local group, lnum, col_start, col_end = unpack(hl)
- vim.api.nvim_buf_add_highlight(bufnr, ns, group, lnum - 1, col_start, col_end)
+---@param bufnr integer
+---@param ns integer
+---@param lines overseer.TextChunk[][]
+M.render_buf_chunks = function(bufnr, ns, lines)
+ if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then
+ return
+ end
+ vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
+ local new_lines = {}
+ local extmarks = {}
+ for _, chunks in ipairs(lines) do
+ local line = {}
+ local i = 0
+ for _, chunk in ipairs(chunks) do
+ ---@cast chunk overseer.TextChunk
+ local text, hl = chunk[1], chunk[2]
+ assert(type(text) == "string", "Text chunk must have a string as the first element")
+ table.insert(line, text)
+ if hl then
+ table.insert(extmarks, { #new_lines, i, { hl_group = hl, end_col = i + #text } })
+ end
+ i = i + #text
+ end
+ local line_text = table.concat(line, ""):gsub("\n", " ")
+ table.insert(new_lines, line_text)
+ end
+ vim.bo[bufnr].modifiable = true
+ vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, new_lines)
+ vim.bo[bufnr].modifiable = false
+ vim.bo[bufnr].modified = false
+ for _, extmark in ipairs(extmarks) do
+ vim.api.nvim_buf_set_extmark(bufnr, ns, extmark[1], extmark[2], extmark[3])
end
end
@@ -171,15 +197,18 @@ end
---@param text string
---@param width number
---@param alignment "left"|"right"|"center"
+---@return string
M.align = function(text, width, alignment)
+ local textwidth = vim.api.nvim_strwidth(text)
if alignment == "center" then
- local padding = math.floor((width - string.len(text)) / 2)
- return string.rep(" ", padding) .. text
+ local padding = math.floor((width - textwidth) / 2)
+ return string.rep(" ", padding) .. text .. string.rep(" ", width - textwidth - padding)
elseif alignment == "right" then
- local padding = width - string.len(text)
+ local padding = width - textwidth
return string.rep(" ", padding) .. text
else
- return text
+ local padding = width - textwidth
+ return text .. string.rep(" ", padding)
end
end
@@ -486,75 +515,11 @@ M.set_bufenter_callback = function(bufnr, key, callback)
})
end
----@param group string
----@return nil|integer
-M.get_hl_foreground = function(group)
- if vim.fn.has("nvim-0.9") == 1 then
- return vim.api.nvim_get_hl(0, { name = group }).fg
- else
- ---@diagnostic disable-next-line: undefined-field, deprecated
- local ok, data = pcall(vim.api.nvim_get_hl_by_name, group, true)
- if ok then
- return data.foreground
- end
- end
-end
-
----@param color integer
----@return number[]
-M.color_to_rgb = function(color)
- local r = bit.band(bit.rshift(color, 16), 0xff)
- local g = bit.band(bit.rshift(color, 8), 0xff)
- local b = bit.band(color, 0xff)
- return { r / 255.0, g / 255.0, b / 255.0 }
-end
-
--- Attempts to find a green color from the current colorscheme
-M.find_success_color = function()
- if vim.fn.has("nvim-0.9") == 1 then
- return "DiagnosticOk"
- end
- local candidates = {
- "Constant",
- "Keyword",
- "Special",
- "Type",
- "PreProc",
- "Operator",
- "String",
- "Statement",
- "Identifier",
- "Function",
- "Character",
- "Title",
- }
- local best_grp
- local best
- for _, grp in ipairs(candidates) do
- local fg = M.get_hl_foreground(grp)
- if fg then
- local rgb = M.color_to_rgb(fg)
- -- Super simple "green" detection heuristic: g - r - b
- local score = rgb[2] - rgb[1] - rgb[3]
- if not best or score > best then
- best_grp = grp
- best = score
- end
- end
- end
- if best_grp and best > -0.5 then
- return best_grp
- end
- return "DiagnosticInfo"
-end
-
---@param func fun(...: any)
---@param opts? {reset_timer_on_call: nil|boolean, delay: nil|integer|fun(...: any): integer}
M.debounce = function(func, opts)
- vim.validate({
- func = { func, "f" },
- opts = { opts, "t", true },
- })
+ vim.validate("func", func, "function")
+ vim.validate("opts", opts, "table", true)
opts = opts or {}
local delay = opts.delay or 300
local timer = nil
@@ -571,7 +536,7 @@ M.debounce = function(func, opts)
if type(delay) == "function" then
delay = delay(unpack(args))
end
- timer = assert(vim.loop.new_timer())
+ timer = assert(vim.uv.new_timer())
timer:start(delay, 0, function()
timer:close()
timer = nil
@@ -603,16 +568,46 @@ M.format_duration = function(duration)
return time
end
+---@param time integer
+---@return string
+M.format_relative_timestamp = function(time)
+ local from_now = time - os.time()
+ local suffix = ""
+ if from_now <= 0 and from_now > -5 then
+ return "just now"
+ elseif from_now > 0 and from_now <= 5 then
+ return "a few seconds"
+ end
+ if from_now < 0 then
+ from_now = -from_now
+ suffix = " ago"
+ end
+ local secs = from_now % 60
+ local days = math.floor(from_now / day_s)
+ local hours = math.floor((from_now % day_s) / hour_s)
+ local mins = math.floor((from_now % hour_s) / minute_s)
+ if days > 0 then
+ return string.format("%d day%s%s", days, days > 1 and "s" or "", suffix)
+ elseif hours > 0 then
+ return string.format("%d hour%s%s", hours, hours > 1 and "s" or "", suffix)
+ elseif mins > 0 then
+ return string.format("%d minute%s%s", mins, mins > 1 and "s" or "", suffix)
+ else
+ return string.format("%d second%s%s", secs, secs > 1 and "s" or "", suffix)
+ end
+end
+
---@param name_or_config string|table
---@param cb fun(task: nil|overseer.Task)
M.run_template_or_task = function(name_or_config, cb)
if type(name_or_config) == "table" and name_or_config[1] == nil then
-- This is a raw task params table
+ ---@cast name_or_config overseer.TaskDefinition
cb(require("overseer").new_task(name_or_config))
else
local name, dep_params = M.split_config(name_or_config)
-- If no task ID found, start the dependency
- require("overseer.commands").run_template({
+ require("overseer").run_task({
name = name,
params = dep_params,
autostart = false,
@@ -625,37 +620,53 @@ end
---@param callback fun()
M.run_in_fullscreen_win = function(bufnr, callback)
if not bufnr or bufnr == 0 then
- bufnr = vim.api.nvim_get_current_buf()
+ bufnr = vim.api.nvim_create_buf(false, true)
+ vim.bo[bufnr].bufhidden = "wipe"
end
local start_winid = vim.api.nvim_get_current_win()
- local winid = vim.api.nvim_open_win(bufnr, false, {
+ local eventignore = vim.o.eventignore
+ vim.o.eventignore = "all"
+ local winid = vim.api.nvim_open_win(bufnr, true, {
relative = "editor",
width = vim.o.columns,
height = vim.o.lines,
row = 0,
col = 0,
- noautocmd = true,
})
- local winnr = vim.api.nvim_win_get_number(winid)
- vim.cmd.wincmd({ count = winnr, args = { "w" }, mods = { noautocmd = true } })
local ok, err = xpcall(callback, debug.traceback)
if not ok then
- vim.api.nvim_err_writeln(err)
+ vim.api.nvim_echo({ { err } }, true, { err = true })
+ end
+ pcall(vim.api.nvim_win_close, winid, true)
+ vim.api.nvim_set_current_win(start_winid)
+ vim.o.eventignore = eventignore
+end
+
+---@param callback fun()
+M.eventignore_call = function(callback)
+ local eventignore = vim.o.eventignore
+ vim.o.eventignore = "all"
+ local ok, err = xpcall(callback, debug.traceback)
+ vim.o.eventignore = eventignore
+ if not ok then
+ error(err)
end
- winnr = vim.api.nvim_win_get_number(winid)
- vim.cmd.close({ count = winnr, mods = { noautocmd = true, emsg_silent = true } })
- winnr = vim.api.nvim_win_get_number(start_winid)
- vim.cmd.wincmd({ count = winnr, args = { "w" }, mods = { noautocmd = true } })
end
---Run a function in the context of a current directory
----@param cwd string
+---@param cwd? string
---@param callback fun()
M.run_in_cwd = function(cwd, callback)
- M.run_in_fullscreen_win(nil, function()
- vim.cmd.lcd({ args = { cwd }, mods = { silent = true, noautocmd = true } })
- callback()
- end)
+ if not cwd then
+ return callback()
+ end
+ local prev_cwd = vim.fn.getcwd()
+ vim.cmd.lcd({ args = { cwd }, mods = { emsg_silent = true, noautocmd = true } })
+ local ok, err = xpcall(callback, debug.traceback)
+ if not ok then
+ vim.api.nvim_echo({ { err } }, true, { err = true })
+ end
+ vim.cmd.lcd({ args = { prev_cwd }, mods = { emsg_silent = true, noautocmd = true } })
end
---@param status overseer.Status
@@ -684,23 +695,6 @@ M.soft_delete_buf = function(bufnr)
end
end
----This is a hack so we don't end up in insert mode after starting a task
----@param prev_mode string The vim mode we were in before opening a terminal
-M.hack_around_termopen_autocmd = function(prev_mode)
- -- It's common to have autocmds that enter insert mode when opening a terminal
- vim.defer_fn(function()
- local new_mode = vim.api.nvim_get_mode().mode
- if new_mode ~= prev_mode then
- if string.find(new_mode, "i") == 1 then
- vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, true, true), "n", false)
- if string.find(prev_mode, "v") == 1 or string.find(prev_mode, "V") == 1 then
- vim.cmd.normal({ bang = true, args = { "gv" } })
- end
- end
- end
- end, 10)
-end
-
---@param old_bufnr nil|integer
---@param new_bufnr nil|integer
M.replace_buffer_in_wins = function(old_bufnr, new_bufnr)
@@ -745,4 +739,71 @@ M.get_last_output_lines = function(bufnr, num_lines)
return lines
end
+---@class overseer.Caller
+---@field file? string
+---@field lnum? integer
+---@field module? string
+---@field top_module? string
+
+---@param file string
+---@param caller overseer.Caller
+local function assign_module(file, caller)
+ local relpath
+ for path in vim.gsplit(vim.o.runtimepath, ",", { plain = true }) do
+ path = path .. "/lua"
+ if file:find(path, 1, true) == 1 then
+ relpath = file:sub(#path + 2)
+ break
+ end
+ end
+ if relpath then
+ local mod = vim.fn.fnamemodify(relpath, ":r"):gsub("[/\\]", ".")
+ local top_mod
+ local dot_idx = mod:find(".", 1, true)
+ if dot_idx then
+ top_mod = mod:sub(1, dot_idx - 1)
+ else
+ top_mod = mod
+ end
+ caller.module = mod
+ caller.top_module = top_mod
+ end
+end
+
+---@return overseer.Caller
+M.get_caller = function()
+ -- 1: this function
+ -- 2: the wrapper function in init.lua
+ -- 3: the actual caller of the jobstart/system function
+ local level = 3
+ local info
+ while true do
+ info = debug.getinfo(level, "Sl")
+ if not info then
+ log.trace("No source info found: %s", debug.traceback())
+ return {}
+ end
+ if info.what ~= "C" then
+ break
+ end
+ level = level + 1
+ end
+ local file, lnum
+ if not info.source:match("^@") then
+ log.trace("Source is not file: %s\n%s", info.source, debug.traceback())
+ return {}
+ end
+
+ file = info.source:sub(2)
+ lnum = info.currentline
+ local ret = { file = file, lnum = lnum }
+ if vim.in_fast_event() then
+ -- This reads runtimepath and uses fnamemodify, which are not safe in fast events
+ vim.schedule_wrap(assign_module)(file, ret)
+ else
+ assign_module(file, ret)
+ end
+ return ret
+end
+
return M
diff --git a/lua/overseer/template/vscode/init.lua b/lua/overseer/vscode/init.lua
similarity index 76%
rename from lua/overseer/template/vscode/init.lua
rename to lua/overseer/vscode/init.lua
index 4846603d..e7ef7261 100644
--- a/lua/overseer/template/vscode/init.lua
+++ b/lua/overseer/vscode/init.lua
@@ -1,11 +1,17 @@
local constants = require("overseer.constants")
-local files = require("overseer.files")
local log = require("overseer.log")
-local problem_matcher = require("overseer.template.vscode.problem_matcher")
-local variables = require("overseer.template.vscode.variables")
-local vs_util = require("overseer.template.vscode.vs_util")
+local problem_matcher = require("overseer.vscode.problem_matcher")
+local variables = require("overseer.vscode.variables")
-local LAUNCH_CONFIG_KEY = "__launch_config__ "
+local M = {}
+
+M.LAUNCH_CONFIG_KEY = "__launch_config__ "
+
+---@class (exact) overseer.VSCodeTaskProvider
+---@field problem_patterns? table
+---@field problem_matchers? table