Skip to content

Commit

Permalink
Add visual mode support (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
piersolenski committed Sep 1, 2023
1 parent e83199a commit 3a951f2
Show file tree
Hide file tree
Showing 16 changed files with 412 additions and 137 deletions.
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,18 @@ jobs:
with:
commit_message: "Auto generate docs"
branch: ${{ github.head_ref }}
test:
runs-on: ubuntu-latest
strategy:
matrix:
nvim-versions: ['stable', 'nightly']
name: test
steps:
- name: checkout
uses: actions/checkout@v3
- uses: rhysd/action-setup-vim@v1
with:
neovim: true
version: ${{ matrix.nvim-versions }}
- name: run tests
run: make test
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
TESTS_INIT=tests/minimal_init.lua
TESTS_DIR=tests/

.PHONY: test

test:
@nvim \
--headless \
--noplugin \
-u ${TESTS_INIT} \
-c "PlenaryBustedDirectory ${TESTS_DIR} { minimal_init = '${TESTS_INIT}' }"
43 changes: 24 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Works with any language that has [LSP](https://microsoft.github.io/language-serv

Use the power of [ChatGPT](https://openai.com/blog/chatgpt) to provide you with explanations *and* solutions for how to fix diagnostics, custom tailored to the code responsible for them.

https://github.com/piersolenski/wtf.nvim/assets/1285419/9b7ab8b1-2dc4-4a18-8051-68745305198a
https://github.com/piersolenski/wtf.nvim/assets/1285419/7572b101-664c-4069-aa45-84adc2678e25

### Search the web for answers

Expand Down Expand Up @@ -44,12 +44,11 @@ use({
dependencies = {
"MunifTanjim/nui.nvim",
},
event = "VeryLazy",
opts = {},
keys = {
{
"gw",
mode = { "n" },
mode = { "n", "x" },
function()
require("wtf").ai()
end,
Expand All @@ -73,44 +72,50 @@ use({
{
-- Default AI popup type
popup_type = "popup" | "horizontal" | "vertical",
-- An alternative way to set your OpenAI api key
-- An alternative way to set your API key
openai_api_key = "sk-xxxxxxxxxxxxxx",
-- ChatGPT Model
openai_model_id = "gpt-3.5-turbo",
-- Send code as well as diagnostics
context = true,
-- Set your preferred language for the response
language = "english",
-- Any additional instructions
additional_instructions = "Start the reply with 'OH HAI THERE'",
-- Default search engine, can be overridden by passing an option to WtfSeatch
-- Default search engine, can be overridden by passing an option to WtfSeatch
search_engine = "google" | "duck_duck_go" | "stack_overflow" | "github",
-- Callbacks
hooks = {
request_started = nil,
request_finished = nil,
},
}
```


## 🚀 Usage

`wtf.nvim` works by sending the line's diagnostic messages along with contextual information (such as the offending code, file type and severity level) to various sources you can configure.

To use it, whenever you have an hint, warning or error in an LSP enabled environment, invoke one of the commands anywhere on that line in Normal mode:
To use it, whenever you have an hint, warning or error in an LSP enabled environment, invoke one of the commands:

| Command | Description |
| -- | -- |
| `:Wtf [additional_instructions]` | Sends the current line along with all diagnostic messages to ChatGPT. Additional instructions can also be specified, which might be useful if you want to refine the response further.
| `:WtfSearch [search_engine]` | Uses a search engine (defaults to the one in the setup or Google if not provided) to search for the **first** diagnostic. It will attempt to filter out unrelated strings specific to your local environment, such as file paths, for broader results.
| Command | Modes | Description |
| -- | -- | -- |
| `:Wtf [additional_instructions]` | Normal, Visual | Sends the diagnostic messages for a line or visual range to ChatGPT, along with the code if the context has been set to `true`. Additional instructions can also be specified, which might be useful if you want to refine the response further.
| `:WtfSearch [search_engine]` | Normal | Uses a search engine (defaults to the one in the setup or Google if not provided) to search for the **first** diagnostic. It will attempt to filter out unrelated strings specific to your local environment, such as file paths, for broader results.

### Custom status hooks

You can add custom hooks to update your status line or other UI elements, for example, this code updates the status line colour to yellow whilst the request is in progress.

```lua
vim.g["wtf_hooks"] = {
request_started = function()
vim.cmd("hi StatusLine ctermbg=NONE ctermfg=yellow")
end,
request_finished = vim.schedule_wrap(function()
vim.cmd("hi StatusLine ctermbg=NONE ctermfg=NONE")
end)
}
hooks = {
request_started = function()
vim.cmd("hi StatusLine ctermbg=NONE ctermfg=yellow")
end,
request_finished = vim.schedule_wrap(function()
vim.cmd("hi StatusLine ctermbg=NONE ctermfg=NONE")
end),
},
```

### Lualine Status Component
Expand Down
78 changes: 50 additions & 28 deletions lua/wtf/ai.lua
Original file line number Diff line number Diff line change
@@ -1,65 +1,87 @@
local get_diagnostics = require("wtf.get_diagnostics")
local get_filetype = require("wtf.get_filetype")
local get_programming_language = require("wtf.utils.get_programming_language")
local gpt = require("wtf.gpt")
local display_popup = require("wtf.display_popup")
local config = require("wtf.config")

local function get_default_additional_instructions()
return vim.g.wtf_default_additional_instructions or ""
end
local M = {}

local function get_language()
return vim.g.wtf_language
local function get_content_between_lines(start_line, end_line)
local lines = {}
for line_num = start_line, end_line do
local line = string.format("%d: %s", line_num, vim.fn.getline(line_num))
table.insert(lines, line)
end
return table.concat(lines, "\n")
end

local ai = function(additional_instructions)
local diagnostics = get_diagnostics()
local filetype = get_filetype()
M.diagnose = function(line1, line2, instructions)
local diagnostics = get_diagnostics(line1, line2)
local programming_language = get_programming_language()
local should_send_code = config.options.context

if diagnostics == nil then
return print("No diagnostics found!")
if next(diagnostics) == nil then
local message = "No diagnostics found!"
vim.notify(message, vim.log.levels.WARN)
return message
end

local concatenatedDiagnostics = ""
for _, diagnostic in ipairs(diagnostics) do
concatenatedDiagnostics = concatenatedDiagnostics .. diagnostic.severity .. ": " .. diagnostic.message .. "\n"
for i, diagnostic in ipairs(diagnostics) do
concatenatedDiagnostics = concatenatedDiagnostics
.. i
.. ". Issue "
.. i
.. "\n\t- Location: Line "
.. diagnostic.line_number
.. "\n\t- Severity: "
.. diagnostic.severity
.. "\n\t- Message: "
.. diagnostic.message
.. "\n"
end

local line = vim.fn.getline(".")
local code = get_content_between_lines(line1, line2)

local payload = "The coding language is "
.. filetype
local payload = "The programming language is "
.. programming_language
.. ".\nThis is a list of the diagnostic messages: \n"
.. concatenatedDiagnostics
.. "This is the line of code for context: \n"
.. line

if get_default_additional_instructions() ~= "" then
payload = payload .. "\n" .. get_default_additional_instructions()
if should_send_code then
payload = payload .. "This is the code for context: \n" .. "```\n" .. code .. "\n```"
end

if additional_instructions then
payload = payload .. "\n" .. additional_instructions
if config.options.additional_instructions then
payload = payload .. "\n" .. config.options.additional_instructions
end

if get_language() ~= "" and get_language() ~= "english" then
payload = payload .. "\nRespond only in " .. get_language()
if instructions then
payload = payload .. "\n" .. instructions
end

print("Generating explanation...")
local language = config.options.language
if language and language ~= "english" then
payload = payload .. "\nRespond only in " .. language
end

vim.notify("Generating explanation...", vim.log.levels.INFO)

local messages = {
{
role = "system",
content = [[You are an expert coder and helpful assistant who can help debug code diagnostics, such as warning and error messages.
When appropriate, give solutions with code snippets as fenced codeblocks with a language identifier to enable syntax highlighting."]],
When appropriate, give solutions with code snippets as fenced codeblocks with a language identifier to enable syntax highlighting.]],
},
{
role = "user",
content = payload,
},
}

gpt.request(messages, display_popup)
return gpt.request(messages, display_popup)
end

return ai
M.get_status = gpt.get_status

return M
71 changes: 71 additions & 0 deletions lua/wtf/config.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
local search_engines = require("wtf.search_engines")

local M = {}

M.options = {}

function M.setup(opts)
local default_opts = {
openai_api_key = nil,
openai_model_id = "gpt-3.5-turbo",
language = "english",
search_engine = "google",
context = true,
additional_instructions = nil,
popup_type = "popup",
hooks = {
request_started = nil,
request_finished = nil,
},
}

-- Merge default_opts with opts
opts = vim.tbl_deep_extend("force", default_opts, opts or {})

vim.validate({
openai_api_key = { opts.openai_api_key, { "string", "nil" } },
openai_model_id = { opts.openai_model_id, "string" },
language = { opts.language, "string" },
search_engine = {
opts.search_engine,
function(search_engine)
local selected_engine = search_engines.sources[search_engine]

if not selected_engine then
return false
else
return true
end
end,
"supported search engine",
},
context = { opts.context, "boolean" },
additional_instructions = { opts.additional_instructions, { "string", "nil" } },
popup_type = {
opts.popup_type,
function(popup_type)
local popup_types = { "horizontal", "vertical", "popup" }

for _, valid_type in ipairs(popup_types) do
if popup_type == valid_type then
return true
end
end
return false
end,
"supported popup type",
},
request_started = {
opts.hooks.request_started,
{ "function", "nil" },
},
request_finished = {
opts.hooks.request_finished,
{ "function", "nil" },
},
})

M.options = vim.tbl_extend("force", M.options, opts)
end

return M
18 changes: 7 additions & 11 deletions lua/wtf/display_popup.lua
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
local Split = require("nui.split")
local Popup = require("nui.popup")

local function get_popup_type()
return vim.g.wtf_popup_type
end
local config = require("wtf.config")

local function split_string_by_line(text)
local lines = {}
Expand Down Expand Up @@ -40,7 +37,7 @@ local function display_popup(responseTable)
},
}

local popup_type = get_popup_type()
local popup_type = config.options.popup_type

if popup_type == "vertical" then
popup = Split(vim.tbl_deep_extend("keep", popup_opts, {
Expand All @@ -67,8 +64,12 @@ local function display_popup(responseTable)
style = "rounded",
},
}))
-- unmount component when cursor leaves buffer
popup:on(event.BufLeave, function()
popup:unmount()
end)
else
return print("Invalid popup_type.")
return vim.notify("Invalid popup type", vim.log.levels.ERROR)
end

vim.api.nvim_buf_set_lines(popup.bufnr, 0, 1, false, message)
Expand All @@ -81,11 +82,6 @@ local function display_popup(responseTable)
popup:update_layout()
end,
})

-- unmount component when cursor leaves buffer
popup:on(event.BufLeave, function()
popup:unmount()
end)
end

return display_popup
Loading

0 comments on commit 3a951f2

Please sign in to comment.