Add :NeuralExplain

Add a new command for visual mode to explain the highlighted lines of
text. Explanations will be printed to a preview window.
This commit is contained in:
w0rp
2023-09-22 01:08:44 +01:00
parent dc15c31470
commit 6993da2ea8
17 changed files with 578 additions and 49 deletions
+17 -1
View File
@@ -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+
@@ -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
+36 -22
View File
@@ -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
@@ -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
@@ -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.
@@ -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
@@ -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
@@ -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)
@@ -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
@@ -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()
@@ -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
@@ -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)},
@@ -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
+90
View File
@@ -0,0 +1,90 @@
" Author: w0rp <devw0rp@gmail.com>
" 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
View File
@@ -0,0 +1,27 @@
" Author: w0rp <devw0rp@gmail.com>
" 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
View File
@@ -0,0 +1,36 @@
" Author: w0rp <devw0rp@gmail.com>
" 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
View File
@@ -0,0 +1,30 @@
" Author: w0rp <devw0rp@gmail.com>
" 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
View File
@@ -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
+2 -2
View File
@@ -41,7 +41,7 @@ function Neural.prompt()
end
function Neural.start_animated_sign(line)
local sign_enabled = vim.g.neural.ui.animated_sign_enabled
local sign_enabled = vim.g.neural and vim.g.neural.ui.animated_sign_enabled
if has_significant and (sign_enabled and sign_enabled ~= 0) and line > 0 then
AnimatedSign.start_animated_sign(line, 'dots', 100)
@@ -49,7 +49,7 @@ function Neural.start_animated_sign(line)
end
function Neural.stop_animated_sign(line)
local sign_enabled = vim.g.neural.ui.animated_sign_enabled
local sign_enabled = vim.g.neural and vim.g.neural.ui.animated_sign_enabled
if has_significant and (sign_enabled and sign_enabled ~= 0) and line > 0 then
AnimatedSign.stop_animated_sign(line, {unplace_sign=true})
+29 -2
View File
@@ -143,6 +143,30 @@ def load_config(raw_config: Dict[str, Any]) -> Config:
)
def get_error_message(error: urllib.error.HTTPError) -> str:
message = error.read().decode('utf-8', errors='ignore')
try:
# JSON data might look like this:
# {
# "error": {
# "message": "...",
# "type": "...",
# "param": null,
# "code": null
# }
# }
message = json.loads(message)['error']['message']
if "This model's maximum context length is" in message:
message = 'Too much text for a request!'
except Exception:
# If we can't get a better message use the JSON payload at least.
pass
return message
def main() -> None:
input_data = json.loads(sys.stdin.readline())
@@ -153,8 +177,11 @@ def main() -> None:
try:
get_chatgpt_completion(config, input_data["prompt"])
except urllib.error.URLError as error:
if isinstance(error, urllib.error.HTTPError) and error.code == 429:
except urllib.error.HTTPError as error:
if error.code == 400:
message = get_error_message(error)
sys.exit('Neural error: OpenAI request failure: ' + message)
elif error.code == 429:
sys.exit("Neural error: OpenAI request limit reached!")
else:
raise
+29 -2
View File
@@ -131,6 +131,30 @@ def load_config(raw_config: Dict[str, Any]) -> Config:
)
def get_error_message(error: urllib.error.HTTPError) -> str:
message = error.read().decode('utf-8', errors='ignore')
try:
# JSON data might look like this:
# {
# "error": {
# "message": "...",
# "type": "...",
# "param": null,
# "code": null
# }
# }
message = json.loads(message)['error']['message']
if "This model's maximum context length is" in message:
message = 'Too much text for a request!'
except Exception:
# If we can't get a better message use the JSON payload at least.
pass
return message
def main() -> None:
input_data = json.loads(sys.stdin.readline())
@@ -141,8 +165,11 @@ def main() -> None:
try:
get_openai_completion(config, input_data["prompt"])
except urllib.error.URLError as error:
if isinstance(error, urllib.error.HTTPError) and error.code == 429:
except urllib.error.HTTPError as error:
if error.code == 400:
message = get_error_message(error)
sys.exit('Neural error: OpenAI request failure: ' + message)
elif error.code == 429:
sys.exit("Neural error: OpenAI request limit reached!")
else:
raise
+5
View File
@@ -29,12 +29,17 @@ if !s:has_features
finish
endif
" Have Neural write to the buffer given a prompt.
command! -nargs=? Neural :call neural#Prompt(<q-args>)
" Stop Neural doing anything.
command! -nargs=0 NeuralStop :call neural#Stop()
" Have Neural explain the visually selected lines.
command! -range NeuralExplain :call neural#explain#SelectedLines()
" <Plug> mappings for commands
nnoremap <silent> <Plug>(neural_prompt) :call neural#OpenPrompt()<Return>
nnoremap <silent> <Plug>(neural_stop) :call neural#Stop()<Return>
vnoremap <silent> <Plug>(neural_explain) :NeuralExplain<Return>
" Set default keybinds for Neural unless we're told not to. We should almost
" never define keybinds by default in a plugin, but we can add only a few to
+43 -5
View File
@@ -3,7 +3,7 @@ import sys
import urllib.error
import urllib.request
from io import BytesIO
from typing import Any, Dict, cast
from typing import Any, Dict, Optional, cast
from unittest import mock
import pytest
@@ -144,7 +144,44 @@ def test_main_function_bad_config():
assert str(exc.value) == 'expect this'
def test_main_function_rate_limit_error():
@pytest.mark.parametrize(
'code, error_text, expected_message',
(
pytest.param(
429,
None,
'OpenAI request limit reached!',
id="request_limit",
),
pytest.param(
400,
'{]',
'OpenAI request failure: {]',
id="error_with_mangled_json",
),
pytest.param(
400,
json.dumps({'error': {}}),
'OpenAI request failure: {"error": {}}',
id="error_with_missing_message_key",
),
pytest.param(
400,
json.dumps({
'error': {
'message': "This model's maximum context length is 123",
},
}),
'OpenAI request failure: Too much text for a request!',
id="too_much_text",
),
)
)
def test_api_error(
code: int,
error_text: Optional[str],
expected_message: str,
):
with mock.patch.object(sys.stdin, 'readline') as readline_mock, \
mock.patch.object(chatgpt, 'get_chatgpt_completion') as compl_mock:
@@ -152,9 +189,10 @@ def test_main_function_rate_limit_error():
url='',
msg='',
hdrs=mock.Mock(),
fp=None,
code=429,
fp=BytesIO(error_text.encode('utf-8')) if error_text else None,
code=code,
)
readline_mock.return_value = json.dumps({
"config": get_valid_config(),
"prompt": "hello there",
@@ -163,4 +201,4 @@ def test_main_function_rate_limit_error():
with pytest.raises(SystemExit) as exc:
chatgpt.main()
assert str(exc.value) == 'Neural error: OpenAI request limit reached!'
assert str(exc.value) == f'Neural error: {expected_message}'
+45 -7
View File
@@ -3,7 +3,7 @@ import sys
import urllib.error
import urllib.request
from io import BytesIO
from typing import Any, Dict, cast
from typing import Any, Dict, Optional, cast
from unittest import mock
import pytest
@@ -137,17 +137,55 @@ def test_main_function_bad_config():
assert str(exc.value) == 'expect this'
def test_main_function_rate_limit_error():
@pytest.mark.parametrize(
'code, error_text, expected_message',
(
pytest.param(
429,
None,
'OpenAI request limit reached!',
id="request_limit",
),
pytest.param(
400,
'{]',
'OpenAI request failure: {]',
id="error_with_mangled_json",
),
pytest.param(
400,
json.dumps({'error': {}}),
'OpenAI request failure: {"error": {}}',
id="error_with_missing_message_key",
),
pytest.param(
400,
json.dumps({
'error': {
'message': "This model's maximum context length is 123",
},
}),
'OpenAI request failure: Too much text for a request!',
id="too_much_text",
),
)
)
def test_api_error(
code: int,
error_text: Optional[str],
expected_message: str,
):
with mock.patch.object(sys.stdin, 'readline') as readline_mock, \
mock.patch.object(openai, 'get_openai_completion') as completion_mock:
mock.patch.object(openai, 'get_openai_completion') as compl_mock:
completion_mock.side_effect = urllib.error.HTTPError(
compl_mock.side_effect = urllib.error.HTTPError(
url='',
msg='',
hdrs=mock.Mock(),
fp=None,
code=429,
fp=BytesIO(error_text.encode('utf-8')) if error_text else None,
code=code,
)
readline_mock.return_value = json.dumps({
"config": get_valid_config(),
"prompt": "hello there",
@@ -156,4 +194,4 @@ def test_main_function_rate_limit_error():
with pytest.raises(SystemExit) as exc:
openai.main()
assert str(exc.value) == 'Neural error: OpenAI request limit reached!'
assert str(exc.value) == f'Neural error: {expected_message}'
+113
View File
@@ -0,0 +1,113 @@
Before:
Save g:neural
" Load modules so we can mock the fuctions.
runtime autoload/neural/job.vim
runtime autoload/neural/visual.vim
runtime autoload/neural/preview.vim
unlet! g:neural
let g:job_id = 0
let g:calls = []
let g:preview_calls = []
let g:OutCb = {->0}
let g:ErrCb = {->0}
let g:ExitCb = {->0}
function! neural#job#Start(command, options) abort
let g:job_id += 1
call add(g:calls, ['neural#job#Start'])
let g:OutCb = a:options.out_cb
let g:ErrCb = a:options.err_cb
let g:ExitCb = a:options.exit_cb
return g:job_id
endfunction
function! neural#job#IsRunning(job_id) abort
return g:job_id == a:job_id
endfunction
function! neural#job#SendRaw(job_id, string) abort
call add(g:calls, ['neural#job#SendRaw', a:job_id, a:string])
endfunction
function! neural#job#Stop(job_id) abort
call add(g:calls, ['neural#job#Stop', a:job_id])
let g:job_id = 0
endfunction
function! neural#visual#GetRange() abort
return g:get_range_result
endfunction
function! neural#preview#Show(lines, options) abort
call add(g:preview_calls, [a:lines, a:options])
endfunction
After:
Restore
unlet! g:job_id
unlet! g:calls
unlet! g:preview_calls
unlet! g:OutCb
unlet! g:ErrCb
unlet! g:ExitCb
unlet! g:get_range_result
" Load actual code again.
runtime autoload/neural/job.vim
runtime autoload/neural/visual.vim
runtime autoload/neural/preview.vim
Given text(Some lines):
One
Two
Three
Four
Five
Six
Execute():
" Set up a mocked result for the range selection.
let g:get_range_result = {
\ 'lnum': 2,
\ 'col': 2,
\ 'end_lnum': 4,
\ 'end_col': 3,
\ 'selection': ['ne', 'Two', 'Three', 'Fou'],
\}
" Run the function :NeuralExplain runs.
call neural#explain#SelectedLines()
call timer_stopall()
" We should send the visually selected lines to the job.
AssertEqual
\ [
\ ['neural#job#Start'],
\ [
\ 'neural#job#SendRaw',
\ 1,
\ json_encode({
\ 'config': g:neural.source[g:neural.selected],
\ 'prompt': "Explain these lines:\n\nne\nTwo\nThree\nFou",
\ }) . "\n",
\ ]
\ ],
\ g:calls
call g:OutCb(g:job_id, 'first output line')
call g:OutCb(g:job_id, 'second output line')
call g:ExitCb(g:job_id, 0)
" We should make a call to preview the lines.
AssertEqual
\ [
\ [
\ ['first output line', 'second output line'],
\ {'stay_here': 1}
\ ],
\ ],
\ g:preview_calls
+8 -8
View File
@@ -1,6 +1,7 @@
Before:
Save g:neural
" Load modules so we can mock the fuctions.
runtime autoload/neural/job.vim
unlet! g:neural
@@ -39,6 +40,8 @@ Before:
call neural#ResetState()
After:
Restore
redir END
unlet! g:job_id
@@ -46,15 +49,15 @@ After:
unlet! g:OutCb
unlet! g:ErrCb
unlet! g:ExitCb
unlet! g:captured_output
augroup WritePostGroup
autocmd!
augroup END
" Load actual code again.
runtime autoload/neural/job.vim
Restore
Given text (A file with two lines and a blank line in the middle):
First line
@@ -71,8 +74,8 @@ Execute(Running Neural and collecting output should work):
autocmd User NeuralWritePost :call add(g:calls, ['NeuralWritePost'])
augroup END
call g:OutCb(g:job_id, "first output line")
call g:OutCb(g:job_id, "second output line")
call g:OutCb(g:job_id, 'first output line')
call g:OutCb(g:job_id, 'second output line')
call g:ExitCb(g:job_id, 0)
AssertEqual
@@ -91,18 +94,16 @@ Execute(Running Neural and collecting output should work):
AssertEqual
\ [
\ ['neural#job#Stop', 0],
\ ['neural#job#Start'],
\ [
\ 'neural#job#SendRaw',
\ 1,
\ json_encode({
\ 'config': g:neural.source.openai,
\ 'config': g:neural.source[g:neural.selected],
\ 'prompt': 'this is a test',
\ }) . "\n",
\ ],
\ ['NeuralWritePost'],
\ ['neural#job#Stop', 1],
\ ],
\ g:calls
@@ -170,7 +171,6 @@ Execute(It should be possible to stop Neural):
AssertEqual
\ [
\ 'neural#job#Stop',
\ 'neural#job#Start',
\ 'neural#job#SendRaw',
\ 'neural#job#Stop',
+24
View File
@@ -0,0 +1,24 @@
After:
unlet! g:preview_data
Execute(Preview windows should be opened correctly):
call neural#preview#Show(['a', 'b'], {'stay_here': 1})
wincmd p
let g:preview_data = {
\ 'lines': getline(1, '$'),
\ 'filetype': &filetype,
\ 'modifiable': &modifiable,
\ 'readonly': &readonly,
\}
:q
" Assert values only after closing the preview window.
" Otherwise the window will remain open.
AssertEqual
\ {
\ 'lines': ['a', 'b'],
\ 'filetype': 'neural-preview',
\ 'modifiable': 0,
\ 'readonly': 1,
\ },
\ g:preview_data
+32
View File
@@ -0,0 +1,32 @@
Execute(Object values for password and secret keys should be redacted):
AssertEqual
\ [
\ '{',
\ ' "password": "*", "SecretKey": r"*",',
\ ' ''password'': ''*'', ''Secret_Key'': r''*'',',
\ ' "password": ''*'', "secret key": r''*'',',
\ ' ''password'': "*", ''secretkey'' : r"*",',
\ '}',
\ ],
\ neural#redact#PasswordsAndSecrets([
\ '{',
\ ' "password": "hideme", "SecretKey": r"hideme",',
\ ' ''password'': ''hideme'', ''Secret_Key'': r''hideme'',',
\ ' "password": ''hideme'', "secret key": r''hideme'',',
\ ' ''password'': "hideme", ''secretkey'' : r"hideme",',
\ '}',
\ ])
Execute(Assignment expressions should be redacted):
AssertEqual
\ ['password = ''*'''],
\ neural#redact#PasswordsAndSecrets(['password = ''hideme'''])
AssertEqual
\ ['password = "*"'],
\ neural#redact#PasswordsAndSecrets(['password = "hideme"'])
AssertEqual
\ ['password = `*`'],
\ neural#redact#PasswordsAndSecrets(['password = `hideme`'])
AssertEqual
\ ['password := `*`'],
\ neural#redact#PasswordsAndSecrets(['password := `hideme`'])