Skip to content

Commit

Permalink
Initial commit. Fairly working implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
sharat87 committed Mar 20, 2019
0 parents commit b9be4f1
Show file tree
Hide file tree
Showing 10 changed files with 616 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# roast.vim

An http client as a ViM editor plugin. Utilizes the `+python3` feature. Not tested on Neovim. Known to work on ViM 8.1.

## Run tests

Go to the python3 directory and run `pytest .`, with Python 3.6 at least.
18 changes: 18 additions & 0 deletions autoload/roast.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
aug roast_response_maps
au!
au BufNewFile __roast_* nnoremap <buffer> <silent> <C-j> :py3 roast.next_render()<CR>
au BufNewFile __roast_* nnoremap <buffer> <silent> <C-k> :py3 roast.prev_render()<CR>
aug END

if (&bg ==? 'light')
highlight default RoastCurrentSuccess guibg=#E7F4D2 gui=bold
highlight default RoastCurrentFailure guibg=#F4DFD2 gui=bold
else
highlight default RoastCurrentSuccess guibg=#005A66 gui=bold
endif

py3 import roast

fun! roast#run()
py3 roast.run()
endfun
209 changes: 209 additions & 0 deletions doc/roast.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
*roast.txt* An HTTP client for ViM

Author: Shrikant Kandula <https://sharats.me>
Repo: https://github.com/sharat87/roast.vim
License: See LICENSE file.

USAGE *roast*

Open (or create) a file with the extension of `.http` or `.roast` and the roast
plugin will be active in that buffer. Now put the following text in that file
and hit the <Enter> key while the cursor is on that line:
>
GET http://httpbin.org/get answer=42
<
The output of this `GET` request should show up in a new vertically split
window. The line itself should highlight in a shade of green. If the request
was a failure, you'd see that the line would highlight in a shade red.


FEATURES OVERVIEW *roast-features*

- Syntax roughly similar to HTTP protocol requests.
- Headers apply to all requests below that header definition line.
- A single session is used for all requests from a buffer. That is, cookies are
preserved across requests from a single buffer.
- Simple string variables support that can be used for interpolation.
- Variable interpolation is implemented with Python's `.format` syntax.
- Variable interpolation works in request paths, query params, header values,
request bodies, other variable definitions.
- Each line is parsed with Python's `shlex` module. That is, comments start with
`#`, quotes should be used to specify strings with special characters such as
spaces.

A good place to find working examples would be the `python3/test_roast.py`
module where several of the above features are tested.


HEADERS *roast-headers*

The syntax for setting headers is similar to how they appear in an actual HTTP
request.
>
Accept: application/json
<
This line will ensure that this header is set to all requests that are below
this line. For example, consider the following file:
>
GET /url/path/1
Accept: application/json
GET /url/path/2
<
If you place the cursor on the request for `/url/path/1` and hit <Enter>, the
`Accept` header is not sent. If, however, you place the cursor on the request
for `/url/path/2` and hit <Enter>, the header is sent. The header is sent for
any and all requests below `/url/path/2` as well.

The header can be given a different value somewhere down and that will be
effective from that point on in the file.

To remove the header, set the header with no value. Like:
>
Accept:
<
No `Accept` header will be sent on the following requests.

TODO: Write about the special handling of `Host` header. This header won't show
up in the headers view.


REQUEST QUERY PARAMS

Query parameters can be given as part of the URL in the usual fashion,
>
GET http://httpbin.org/get?key1=value1&key2=value2
<
Note that the URL is processed with python's `.format` with |roast-variables|
so variable interpolation can be used here.

But it is usually more convenient to use spaces that roast.vim allows instead
of the `&` and `?` marks. The above can be written as:
>
GET http://httpbin.org/get key1=value1 key2=value2
<
In this form, only the values support interpolation. For example,
>
set answer 42
GET http://httpbin.org/get text=number_{answer}
<
This sends a request to the following URL:
>
http://httpbin.org/get?text=number_42
<
There's also a shortcut for the following common use case:
>
set answer 42
GET http://httpbin.org/get answer={answer}
<
This can be written as:
>
set answer 42
GET http://httpbin.org/get answer
<
Both send request to the following URL:
>
http://httpbin.org/get?answer=42
<


REQUEST BODY *roast-post-body*

The body for POST or other requests can be specified using heredoc-like syntax.
This is best illustrated with an example:
>
POST http://httpbin.org/post << body
here goes the post body
this content is passed to the POST request as-is.
body
<
The heredoc marker is `body` here. It can be any alphanumeric string.


USING VARIABLES *roast-variables*

Variables allow for interpolation within braces and this lets us create nice
little DSL's made up of HTTP APIs.

The syntax to set a variable is as following:
>
set name value
<
If the value contains spaces or special characters, it can be surrounded by
quotes, similar to escaping semantics of a shell. Note the variable
interpolations are allowed in the value part of this syntax.

Variable interpolations are powered by Python's `str.format` method. If you're
not familiar with that method, don't worry, here's what it looks like:
>
set first_name Elijah
set last_name Baley
set full_name {first_name}_{last_name}
<
The variable `full_name` will be set to `Elijah_Baley`.

Variable interpolations work in header values, URL's, query parameter values and
other variable declarations. A variable can be set to a different value with the
same file. Only the closest `set` above the current line will be taken into
consideration.

A neat shortcut regarding interpolation in query params is that it is possible
to use params already set for interpolation in later params, with the name
prefixed with the `@` symbol. That doesn't make sense, I know, but an example
will click right away. Here it is:
>
GET https://httpbin.org/get first=Jon last=Snow full={@first}_{@last}
<
Get it now? :)


VIEWING RESPONSE *roast-viewing-response*

When a request is run (by hitting the <Enter> key, by default), a new window is
opened by splitting vertically, unless a window is already displaying a roast
result.

This window will show the response of the query with json syntax highlighting if
the appropriate response header is present.

To view other details of the request/response, go to the window displaying a the
result and hit <C-j> or <C-k> to cycle between several renderers of the response
data.

There is a provision for custom renderers, but it's not mature enough to be
documented and encouraged in the wild. Coming soon.


TIPS *roast-tips*

1. Switch the host header between various staging servers of your app.

For example, if we have http://dev.staging.example.com/ as our dev server and
http://qa.staging.example.com/ as our QA server, we can set the host in our
roast file in the first line like:
>
Host: http://dev.staging.example.com/
<
With this, we can have a hotkey to switch this host between dev and QA hosts.
This can be achieved with something like the following (to be put in
~/.vim/after/ftplugin/roast.vim):

>
py3 import vim
nnoremap <silent> <buffer> <LoacalLeader>d :py3 vim.current.buffer[0] = 'Host: http://dev.staging.example.com/'
nnoremap <silent> <buffer> <LoacalLeader>q :py3 vim.current.buffer[0] = 'Host: http://qa.staging.example.com/'
<

Of course, this is very basic, it assumes the Host header is on the first line or
overwrites that line when the hotkey is hit.


More tips coming soon!


ABOUT *roast-about*

Roast plugin is developed by sharat87. Contributions welcome. If you notice
a bug or have an idea for a feature request, please open an issue on GitHub.


vim:ft=help
1 change: 1 addition & 0 deletions ftdetect/roast.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
au BufRead,BufNewFile *.roast,*.http setf roast
3 changes: 3 additions & 0 deletions ftplugin/roast.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
setl commentstring=#\ %s

nnoremap <buffer> <silent> <CR> :call roast#run()<CR>
120 changes: 120 additions & 0 deletions python3/roast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""
The implementation module for roast.vim plugin. This module does most of the heavy lifting for the functionality
provided by the plugin.
Example: Put the following in a `api.roast` file and hit `<Leader><CR>` on it.
GET http://httpbin.org/get name=value
Inspiration / Ideas:
https://github.com/Huachao/vscode-restclient
https://github.com/baverman/vial-http
"""

from collections import defaultdict

import requests
import vim

import roast_api


sessions = defaultdict(requests.Session)

renderers = [
'pretty',
'headers',
]


def run():
request = roast_api.build_request(vim.current.buffer, vim.current.range.end)

try:
response = sessions[vim.current.buffer.number].send(request.prepare())
except requests.ConnectionError as e:
vim.current.buffer.vars['_roast_error'] = repr(e)
vim.command(f"echoerr b:_roast_error")
else:
show_response(response)
highlight_line_text('RoastCurrentSuccess' if response.ok else 'RoastCurrentFailure')


def show_response(response: requests.Response):
# A window holding a roast buffer, to be used as a workspace for setting up all roast buffers.
workspace_window = workspace_renderer = None
for window in vim.windows:
if '_roast_renderer' in window.buffer.vars:
workspace_window = window
workspace_renderer = window.buffer.vars['_roast_renderer'].decode()
break

# Switch to workspace window.
prev_window = vim.current.window

for renderer in renderers:
buf_name = f'__roast_{renderer}__'
num = bufnr(buf_name)
if num < 0:
if workspace_window is not None:
vim.current.window = workspace_window
vim.command(f'keepalt edit {buf_name} | setl bt=nofile bh=hide noswf nornu')
num = bufnr(buf_name)
else:
vim.command(f'keepalt vnew {buf_name} | setl bt=nofile bh=hide noswf nornu')
num = bufnr(buf_name)
vim.current.window = workspace_window = vim.windows[int(vim.eval(f'bufwinnr({num})')) - 1]
else:
if workspace_window is not None:
vim.current.window = workspace_window
vim.command(f'keepalt {num}buffer')
else:
vim.command(f'keepalt vertical {num}sbuffer')
vim.current.window = workspace_window = vim.windows[int(vim.eval(f'bufwinnr({num})')) - 1]

buf = vim.buffers[num]
buf[:] = None

buf.vars['_roast_renderer'] = renderer
actions = getattr(roast_api, f'render_{renderer}')(buf, response)
apply_actions(buf, actions)

if vim.current.window is workspace_window:
vim.command(f'keepalt buffer __roast_{workspace_renderer or renderers[0]}__')

vim.current.window = prev_window


def highlight_line_text(group):
match_id = int(vim.current.buffer.vars.get('_roast_match_id', 0))

if match_id:
try:
vim.eval(f'matchdelete({match_id})')
except vim.error:
# TODO: Only hide E803 error, which is thrown if this match_id has already been deleted.
pass

vim.current.buffer.vars['_roast_match_id'] = vim.eval(f"matchadd('{group}', '\\V{vim.current.line}')")


def apply_actions(buf, actions):
if 'lines' in actions:
buf[:] = actions['lines']

if 'commands' in actions:
for cmd in actions['commands']:
vim.command(cmd)


def next_render(delta=1):
renderer = vim.current.buffer.vars['_roast_renderer'].decode()
vim.command('buffer __roast_' + renderers[(renderers.index(renderer) + delta) % len(renderers)] + '__')


def prev_render():
next_render(-1)


def bufnr(name) -> int:
return int(vim.eval(f'bufnr("{name}")'))
Loading

0 comments on commit b9be4f1

Please sign in to comment.