Skip to content

Commit

Permalink
Add :NeuralExplain
Browse files Browse the repository at this point in the history
Add a new command for visual mode to explain the highlighted lines of
text. Explanations will be printed to a preview window.
  • Loading branch information
w0rp committed Sep 22, 2023
1 parent dc15c31 commit 6993da2
Show file tree
Hide file tree
Showing 17 changed files with 578 additions and 49 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ A ChatGPT Vim plugin, an OpenAI Neovim plugin, and so much more! Neural integrat

* Generate text easily `:Neural write a story`
* Support for multiple machine learning models
* Focused on privacy and avoiding leaking data to third parties
* Easily ask AI to explain code or paragraphs `:NeuralExplain`
* Compatible with Vim 8.0+ & Neovim 0.8+
* Supported on Linux, Mac OSX, and Windows
* Only dependency is Python 3.7+
Expand Down Expand Up @@ -107,11 +109,25 @@ Try typing `:Neural say hello`, and if all goes well the machine learning
tool will say "hello" to you in the current buffer. Type `:help neural` to
see the full documentation.

## 🛠️ Commands

### `:NeuralExplain`

You can ask Neural to explain code or text by visually selecting it and running
the `:NeuralExplain` command. You may also create a custom keybind for
explaining a visual range with `<Plug>(neural_explain)`.

Neural will make basic attempts to redact lines that appear to contain passwords
or secrets. You may audit this code by reading
[`autoload/neural/redact.vim`](https://github.com/dense-analysis/neural/blob/main/autoload/neural/redact.vim)

### `:NeuralStop`

You can stop Neural from working by with the `NeuralStop` command. Unless
another keybind for `<C-c>` (CTRL+C) is defined in normal mode, Neural will run
the stop command by default when you enter that key combination. The default
keybind can be disabled by setting `g:neural.set_default_keybinds` to any falsy
value.
value. You can set a keybind to stop Neural by mapping to `<Plug>(neural_stop)`.

## 📜 Acknowledgements

Expand Down
58 changes: 36 additions & 22 deletions autoload/neural.vim
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ function! neural#GetScriptDir() abort
return s:neural_script_dir
endfunction

function! s:OutputErrorMessage(message) abort
" Output an error message. The message should be a string.
" The output error lines will be split in a platform-independent way.
function! neural#OutputErrorMessage(message) abort
let l:lines = split(a:message, '\v\r\n|\n|\r')

if len(l:lines) > 0
Expand Down Expand Up @@ -100,7 +102,7 @@ function! s:HandleOutputEnd(buffer, job_data, exit_code) abort

" Output an error message from the program if something goes wrong.
if a:exit_code != 0
call s:OutputErrorMessage(join(a:job_data.error_lines, "\n"))
call neural#OutputErrorMessage(join(a:job_data.error_lines, "\n"))
else
" Signal Neural is done for plugin integration.
silent doautocmd <nomodeline> User NeuralWritePost
Expand All @@ -110,6 +112,8 @@ function! s:HandleOutputEnd(buffer, job_data, exit_code) abort
echomsg 'Neural is done!'
endif
endif

let s:current_job = 0
endfunction

" Get the path to the executable for a script language.
Expand Down Expand Up @@ -151,8 +155,10 @@ function! neural#Escape(str) abort
return shellescape (a:str)
endfunction

" Complain about no prompt text being provided.
" This function is also called from Lua code.
function! neural#ComplainNoPromptText() abort
call s:OutputErrorMessage('No prompt text!')
call neural#OutputErrorMessage('No prompt text!')
endfunction

function! neural#OpenPrompt() abort
Expand All @@ -166,10 +172,6 @@ function! neural#OpenPrompt() abort
endif
endfunction

function! neural#ComplainNoPromptText() abort
call s:OutputErrorMessage('No prompt text!')
endfunction

function! s:InitiallyInformUser(job_id) abort
if neural#job#IsRunning(a:job_id)
" no-custom-checks
Expand All @@ -185,8 +187,17 @@ function! s:InformUserIfStillBusy(job_id) abort
endfunction

function! neural#Cleanup() abort
" Stop any currently running jobs.
call neural#job#Stop(s:current_job)
" Stop :NeuralExplain if it might be running.
if exists('*neural#explain#Cleanup')
call neural#explain#Cleanup()
endif

if s:current_job
" Stop any currently running jobs.
call neural#job#Stop(s:current_job)
let s:current_job = 0
endif

" Stop timers for informing the user.
call timer_stop(s:initial_timer)
call timer_stop(s:busy_timer)
Expand Down Expand Up @@ -229,7 +240,7 @@ function! s:LoadDataSource() abort
try
let l:source = function('neural#source#' . selected . '#Get')()
catch /E117/
call s:OutputErrorMessage('Invalid source: ' . l:selected)
call neural#OutputErrorMessage('Invalid source: ' . l:selected)

return
endtry
Expand All @@ -253,6 +264,16 @@ function! s:GetSourceInput(buffer, source, prompt) abort
return l:input
endfunction

function! neural#GetCommand(buffer) abort
let l:source = s:LoadDataSource()
let l:script_exe = s:GetScriptExecutable(l:source)
let l:command = neural#Escape(l:script_exe)
\ . ' ' . neural#Escape(l:source.script)
let l:command = neural#job#PrepareCommand(a:buffer, l:command)

return [l:source, l:command]
endfunction

function! neural#Prompt(prompt) abort
" Reload the Neural config on a prompt request if needed.
call neural#config#Load()
Expand All @@ -262,7 +283,7 @@ function! neural#Prompt(prompt) abort
if has('nvim') && g:neural.ui.prompt_enabled
call neural#OpenPrompt()
else
call s:OutputErrorMessage('No prompt text!')
call neural#ComplainNoPromptText()
endif

return
Expand All @@ -276,18 +297,12 @@ function! neural#Prompt(prompt) abort
let l:moving_line -= 1
endif

let l:source = s:LoadDataSource()
let l:script_exe = s:GetScriptExecutable(l:source)
let l:command = neural#Escape(l:script_exe)
\ . ' ' . neural#Escape(l:source.script)
let l:command = neural#job#PrepareCommand(l:buffer, l:command)
let [l:source, l:command] = neural#GetCommand(l:buffer)
let l:job_data = {
\ 'moving_line': l:moving_line,
\ 'error_lines': [],
\ 'content_started': 0,
\}
let l:input = s:GetSourceInput(l:buffer, l:source, a:prompt)

let l:job_id = neural#job#Start(l:command, {
\ 'mode': 'nl',
\ 'out_cb': {job_id, line -> s:AddLineToBuffer(l:buffer, l:job_data, line)},
Expand All @@ -296,11 +311,10 @@ function! neural#Prompt(prompt) abort
\})

if l:job_id > 0
let l:stdin_data = json_encode(l:input) . "\n"

call neural#job#SendRaw(l:job_id, l:stdin_data)
let l:input = s:GetSourceInput(l:buffer, l:source, a:prompt)
call neural#job#SendRaw(l:job_id, json_encode(l:input) . "\n")
else
call s:OutputErrorMessage('Failed to run ' . l:source.name)
call neural#OutputErrorMessage('Failed to run ' . l:source.name)

return
endif
Expand Down
90 changes: 90 additions & 0 deletions autoload/neural/explain.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
" Author: w0rp <[email protected]>
" Description: Explain a visual selection with Neural.

let s:current_job = get(s:, 'current_explain_job', 0)

function! s:AddOutputLine(buffer, job_data, line) abort
call add(a:job_data.output_lines, a:line)
endfunction

function! s:AddErrorLine(buffer, job_data, line) abort
call add(a:job_data.error_lines, a:line)
endfunction

function! s:HandleOutputEnd(buffer, job_data, exit_code) abort
" Output an error message from the program if something goes wrong.
if a:exit_code != 0
" Complain when something goes wrong.
call neural#OutputErrorMessage(join(a:job_data.error_lines, "\n"))
else
let l:i = 0

while l:i < len(a:job_data.output_lines)
if !empty(a:job_data.output_lines[l:i])
break
endif

let l:i += 1
endwhile

call neural#preview#Show(
\ a:job_data.output_lines[l:i :],
\ {'stay_here': 1},
\)
endif

let s:current_job = 0
endfunction

function! neural#explain#Cleanup() abort
if s:current_job
call neural#job#Stop(s:current_job)
let s:current_job = 0
endif
endfunction

function! neural#explain#SelectedLines() abort
" Reload the Neural config if needed.
call neural#config#Load()
" Stop Neural doing anything else if explaining code.
call neural#Cleanup()

let l:range = neural#visual#GetRange()
let l:buffer = bufnr('')

let [l:source, l:command] = neural#GetCommand(l:buffer)

let l:job_data = {
\ 'output_lines': [],
\ 'error_lines': [],
\}
let l:job_id = neural#job#Start(l:command, {
\ 'mode': 'nl',
\ 'out_cb': {job_id, line -> s:AddOutputLine(l:buffer, l:job_data, line)},
\ 'err_cb': {job_id, line -> s:AddErrorLine(l:buffer, l:job_data, line)},
\ 'exit_cb': {job_id, exit_code -> s:HandleOutputEnd(l:buffer, l:job_data, exit_code)},
\})

if l:job_id > 0
let l:lines = neural#redact#PasswordsAndSecrets(l:range.selection)

let l:config = get(g:neural.source, l:source.name, {})

" If the config is not a Dictionary, throw it away.
if type(l:config) isnot v:t_dict
let l:config = {}
endif

let l:input = {
\ 'config': l:config,
\ 'prompt': "Explain these lines:\n\n" . join(l:lines, "\n"),
\}
call neural#job#SendRaw(l:job_id, json_encode(l:input) . "\n")
else
call neural#OutputErrorMessage('Failed to run ' . l:source.name)

return
endif

let s:current_job = l:job_id
endfunction
27 changes: 27 additions & 0 deletions autoload/neural/preview.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
" Author: w0rp <[email protected]>
" Description: Preview windows for showing whatever information in.

" Open a preview window and show some lines in it.
" The second argument allows options to be passed in.
"
" filetype - The filetype to use, defaulting to 'neural-preview'
" stay_here - If 1, stay in the window you came from.
function! neural#preview#Show(lines, options) abort
silent pedit NeuralPreviewWindow
wincmd P

setlocal modifiable
setlocal noreadonly
setlocal nobuflisted
setlocal buftype=nofile
setlocal bufhidden=wipe
:%d
call setline(1, a:lines)
setlocal nomodifiable
setlocal readonly
let &l:filetype = get(a:options, 'filetype', 'neural-preview')

if get(a:options, 'stay_here')
wincmd p
endif
endfunction
36 changes: 36 additions & 0 deletions autoload/neural/redact.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
" Author: w0rp <[email protected]>
" Description: Redact passwords and secrets from code.

" This will never be a perfect implementation, but effort can be made.
"
" The regex to use for keys in objects.
let s:key_regex = 'password|secret[_ ]?key'
" This substitution will be applied for each of the s:key_value_groups.
let s:key_value_sub = '\1\3*\4'
" Regular expressions to replace with s:key_value_sub
let s:key_value_groups = map(
\ [
\ '("(KEYS)")( *[=:]+ *r?")[^"]+(")',
\ '("(KEYS)")( *[=:]+ *r?'')[^'']+('')',
\ '(''(KEYS)'')( *[=:]+ *r?'')[^'']+('')',
\ '(''(KEYS)'')( *[=:]+ *r?")[^"]+(")',
\ '((KEYS))( *[=:]+ *r?")[^"]+(")',
\ '((KEYS))( *[=:]+ *r?'')[^'']+('')',
\ '((KEYS))( *[=:]+ *r?`)[^`]+(`)',
\ ],
\ {_, template -> '\v\c' . substitute(template, 'KEYS', s:key_regex, '')}
\)

function! neural#redact#PasswordsAndSecrets(unredacted) abort
let l:line_list = []

for l:line in a:unredacted
for l:regex in s:key_value_groups
let l:line = substitute(l:line, l:regex, s:key_value_sub, 'g')
endfor

call add(l:line_list, l:line)
endfor

return l:line_list
endfunction
30 changes: 30 additions & 0 deletions autoload/neural/visual.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
" Author: w0rp <[email protected]>
" Description: Neural functions for working with Vim's visual mode.

" Get all information for a visual range command we might want.
"
" Credit to xolox for most of this.
function! neural#visual#GetRange() abort
let [l:lnum, l:col] = getpos("'<")[1:2]
let [l:end_lnum, l:end_col] = getpos("'>")[1:2]
let l:end_offset = &selection is# 'inclusive' ? 1 : 2

" Get the line range and slice the text we selected.
let l:selection = getline(l:lnum, l:end_lnum)

if !empty(l:selection)
let l:selection[0] = l:selection[0][l:col - 1:]
let l:selection[-1] = l:selection[-1][: l:end_col - l:end_offset]
" Get the actual end column from the text length, as Vim can give us
" the maximum int for visual line mode.
let l:end_col = len(l:selection[-1])
endif

return {
\ 'lnum': l:lnum,
\ 'col': l:col,
\ 'end_lnum': l:end_lnum,
\ 'end_col': l:end_col,
\ 'selection': l:selection,
\}
endfunction
12 changes: 12 additions & 0 deletions doc/neural.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ Neural *Neural*
completes writing text to a buffer.


NeuralExplain *NeuralExplain*

A |visual-mode| command for explaining the highlighted lines. The visual
selection will be sent to the currently selected source model and the
response will be displayed in a preview window.

Neural will make basic attempts to redact lines that appear to contain
passwords or secrets.

A plug mapping `<Plug>(neural_explain)` is defined for this command.


NeuralStop *NeuralStop*

Stop any currently running Neural tasks, and immediately stop printing text
Expand Down
Loading

0 comments on commit 6993da2

Please sign in to comment.