├── .circleci └── config.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.md ├── autoload ├── http.vim ├── vital.vim └── vital │ ├── _vim_http.vim │ ├── _vim_http │ └── Data │ │ └── Base64.vim │ ├── vim_http.vim │ └── vim_http.vital ├── docker-compose.yml ├── ftdetect └── http.vim ├── plugin └── http.vim ├── syntax └── http.vim └── test ├── .themisrc ├── integration.vim └── test-http-files ├── example_in_docs.md ├── get_body_params.expected.http ├── get_body_params.http ├── get_url_params.expected.http ├── get_url_params.http ├── get_with_multiple_headers.http ├── http_1_0.expected.http ├── http_1_0.http ├── http_1_1.expected.http ├── http_1_1.http ├── http_2.http ├── patch_json.expected.http ├── patch_json.http ├── post_incomplete.http ├── post_invalid_content_length.http ├── post_json.expected.http ├── post_json.http ├── redirect.expected.http ├── redirect.http ├── redirect_follow.expected.http ├── redirect_follow.http ├── simple_get.expected.http ├── simple_get.http └── simple_get_without_protocol.http /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | latest: 4 | docker: 5 | - image: thinca/vim:latest 6 | - image: citizenstig/httpbin 7 | steps: &steps 8 | - run: 9 | name: install git and curl 10 | command: apk add --no-cache git curl 11 | - checkout 12 | - run: 13 | name: test requirements 14 | command: | 15 | git clone https://github.com/thinca/vim-themis 16 | git clone https://github.com/syngan/vim-vimlint /tmp/vim-vimlint 17 | git clone https://github.com/ynkdir/vim-vimlparser /tmp/vim-vimlparser 18 | - run: 19 | name: test 20 | command: | 21 | vim-themis/bin/themis --reporter dot test 22 | - run: 23 | name: lint 24 | command: | 25 | sh /tmp/vim-vimlint/bin/vimlint.sh -l /tmp/vim-vimlint -p /tmp/vim-vimlparser -e EVL102.l:_=1 -c func_abort=1 autoload ftdetect plugin syntax 26 | oldest: 27 | docker: 28 | - image: thinca/vim:v7.4.566 29 | - image: citizenstig/httpbin 30 | steps: *steps 31 | 32 | 33 | workflows: 34 | version: 2 35 | test: 36 | jobs: 37 | - latest 38 | - oldest 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/vim 3 | 4 | ### Vim ### 5 | # swap 6 | [._]*.s[a-w][a-z] 7 | [._]s[a-w][a-z] 8 | # session 9 | Session.vim 10 | # temporary 11 | .netrwhist 12 | *~ 13 | # auto-generated tag files 14 | tags 15 | 16 | vim-themis 17 | vim-vimlint 18 | vim-vimlparser 19 | .vagrant 20 | Vagrantfile 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | - `g:vim_http_tempbuffer` that makes response buffers temporary 10 | [#5](https://github.com/nicwest/vim-http/pull/5). 11 | 12 | 13 | ## [1.0.0] 14 | ### Added 15 | - support for `PATCH` http method 16 | - pin minimum vim version at 7.4.566, if you are using a version prior to 17 | this you can use the version of the code with the 18 | [pre-7.4.566](https://github.com/nicwest/vim-http/tree/pre-7.4.566) tag 19 | 20 | ## [0.0.1] - 2017-08-21 21 | ### Added 22 | - This CHANGELOG file! 23 | - g:vim_http_split_vertically to controll split direction 24 | 25 | 26 | [Unreleased]: https://github.com/nicwest/vim-http/compare/1.0.0...HEAD 27 | [1.0.0]: https://github.com/nicwest/vim-http/compare/0.0.1...1.0.0 28 | [0.0.1]: https://github.com/nicwest/vim-http/compare/TAIL...0.0.1 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | All issues/pull requests are welcome. 5 | 6 | That said, things that I'm unlikely to merge: 7 | 8 | * Deviations from the various HTTP specs 9 | [rfc1945](https://tools.ietf.org/html/rfc1945) 10 | [rfc7230-7235](https://tools.ietf.org/html/rfc7230) 11 | [rfc7540](https://tools.ietf.org/html/rfc7540). 12 | The idea is that a request should be able to be piped into a connection to a 13 | web server (for example via telnet or openssl) and for the server to 14 | recognise the request. 15 | 16 | * Binding things to keyboard shortcuts by default or with some kind of global 17 | flag. People to should be making their own decisions about where they bind 18 | things and/or if they need to bind stuff at all. I'm happy to merge 19 | suggestions into the README. 20 | 21 | * New functionality without tests. 22 | 23 | Support 24 | ------- 25 | 26 | I aim to support versions of vim from 7.4.566 27 | (see https://github.com/nicwest/vim-http/pull/5#issuecomment-472483160) to 28 | whatever the latest version is. 29 | 30 | Any changes to keep this plug-in up to date are welcome. Reasonable changes 31 | that allow us to support older versions are also welcome but are not required. 32 | 33 | Generally speaking I'm looking to not break things for older versions of vim 34 | or curl, however sometimes compromises are needed. In a case where the oldest 35 | supported version increases I'll tag the previous commit for people with older 36 | setups. 37 | 38 | * [<7.4.566](https://github.com/nicwest/vim-http/tree/pre-7.4.566) [#5](https://github.com/nicwest/vim-http/pull/5) 39 | 40 | 41 | Tests 42 | ----- 43 | 44 | The integration test use [httpbin](https://httpbin.org/), and assumes it 45 | running locally. I use a 46 | [docker container](https://github.com/citizen-stig/dockerhttpbin): 47 | 48 | ``` 49 | docker pull citizenstig/httpbin 50 | docker run -d=true -p 8000:8000 citizenstig/httpbin 51 | ``` 52 | 53 | To run the tests pull the 54 | [themis test suite](https://github.com/thinca/vim-themis) 55 | (you don't have to install it but you can if you want). I normally just dump it 56 | in the plugin directory. 57 | 58 | ``` 59 | git clone git@github.com:thinca/vim-themis.git 60 | ./vim-themis/bin/themis --reporter dot test 61 | ``` 62 | 63 | The test responses use a VERY hacked together parsing to match against what 64 | can be a somewhat dynamic set of headers etc. 65 | 66 | Basically it will match anything inside a pair of `%%`: 67 | 68 | 69 | ``` 70 | "User-Agent": "curl/%% '[123.]\+' %%" 71 | ``` 72 | 73 | Would match 74 | 75 | ``` 76 | "User-Agent": "curl/1.2.3" 77 | ``` 78 | 79 | But not 80 | 81 | ``` 82 | "User-Agent": "curl/4.5.6" 83 | ``` 84 | 85 | Or 86 | 87 | ``` 88 | "User-Agent": "curl/latest" 89 | ``` 90 | 91 | Style 92 | ----- 93 | 94 | This project attempts to follow the 95 | [Google Vimscript Style Guide](https://google.github.io/styleguide/vimscriptguide.xml) 96 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test setup httpbin 2 | 3 | test: 4 | vim-themis/bin/themis --reporter dot test 5 | 6 | setup: 7 | docker pull citizenstig/httpbin 8 | git clone git@github.com:thinca/vim-themis.git 9 | git clone git@github.com:syngan/vim-vimlint 10 | git clone git@github.com:ynkdir/vim-vimlparser 11 | 12 | httpbin: 13 | docker run -d=true -p 8000:8000 citizenstig/httpbin 14 | 15 | lint: 16 | ./vim-vimlint/bin/vimlint.sh -l ./vim-vimlint -p ./vim-vimlparser -e EVL102.l:_=1 -c func_abort=1 autoload ftdetect plugin syntax 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/nicwest/vim-http/tree/master.svg?style=svg)](https://circleci.com/gh/nicwest/vim-http/tree/master) 2 | [![Powered by vital.vim](https://img.shields.io/badge/powered%20by-vital.vim-80273f.svg)](https://github.com/vim-jp/vital.vim) 3 | 4 | vim-http 5 | ======== 6 | 7 | Simple wrapper over curl and http syntax highlighting. 8 | 9 | [![asciicast](https://asciinema.org/a/120707.png)](https://asciinema.org/a/120707) 10 | 11 | 12 | Usage 13 | ------ 14 | 15 | Write a raw http request 16 | 17 | ```http 18 | GET http://httpbin.org/get HTTP/1.1 19 | Host: httpbin.org 20 | Accept: application/json 21 | ``` 22 | 23 | `:Http` will execute the request and display the response in a new buffer. 24 | 25 | `:Http!` will execute the request as above and follow any redirects. 26 | 27 | `:HttpShowCurl` displays the curl request that the plugin executes under the 28 | hood 29 | 30 | `:HttpShowRequest` displays the internal object representing the request 31 | 32 | `:HttpClean` will add Host and Content-Length headers 33 | 34 | `:HttpAuth` will prompt for authorization credentials 35 | 36 | 37 | Configuration 38 | ------------- 39 | 40 | `g:vim_http_clean_before_do` if set to `1` (default) will clean a request before 41 | sending it to curl. Disable this by setting this global to `0` 42 | 43 | `g:vim_http_additional_curl_args` can be used to provide additional arguments 44 | to curl. 45 | 46 | `g:vim_http_split_vertically` when set to `1` will split the window vertically 47 | on response rather than horizontally (the default). 48 | 49 | `g:vim_http_right_below` when set to `1` split window will be open on the 50 | right (for vertical) or below (for horizontal). 51 | 52 | `g:vim_http_tempbuffer` when set to `1` response buffers will overwrite each 53 | other instead of persisting forever. 54 | 55 | Helper Methods 56 | -------------- 57 | 58 | `http#remove_header(header)` removes all occurances of the given header in the 59 | current buffer. 60 | 61 | `http#set_header(header, value)` sets the header to the given value in the 62 | current buffer, removes duplicates 63 | 64 | Examples for your vimrc: 65 | 66 | ```viml 67 | function! s:set_json_header() abort 68 | call http#set_header('Content-Type', 'application/json') 69 | endfunction 70 | 71 | function! s:clean_personal_stuff() abort 72 | call http#remove_header('Cookie') 73 | call http#remove_header('Accept') 74 | call http#remove_header('User-Agent') 75 | call http#remove_header('Accept-Language') 76 | endfunction 77 | 78 | function! s:add_compression() abort 79 | call http#set_header('Accept-Encoding', 'deflate, gzip') 80 | let g:vim_http_additional_curl_args = '--compressed' 81 | endfunction 82 | 83 | command! JSON call s:set_json_header() 84 | command! Anon call s:clean_personal_stuff() 85 | command! Compression call s:add_compression() 86 | ``` 87 | -------------------------------------------------------------------------------- /autoload/http.vim: -------------------------------------------------------------------------------- 1 | let s:save_cpo = &cpo 2 | set cpo&vim 3 | 4 | let s:V = vital#vim_http#new() 5 | let s:Base64 = s:V.import('Data.Base64') 6 | 7 | function! s:new_request() abort 8 | let l:response = { 9 | \ 'method': 'GET', 10 | \ 'headers': {}, 11 | \ 'uri': '', 12 | \ 'content': '', 13 | \ 'version': '1.1', 14 | \ 'meta': { 15 | \ 'follow': 0, 16 | \ } 17 | \ } 18 | return l:response 19 | endfunction 20 | 21 | let s:pre_clean_uri_line_pattern = '^\(\a*\) \(.*\) HTTP/\([0-9.]\+\)\|$' 22 | let s:uri_line_pattern = '^\(OPTIONS\|GET\|HEAD\|POST\|PUT\|DELETE\|TRACE\|CONNECT\|PATCH\) \(.*\) HTTP/\([0-9.]\+\)$' 23 | let s:header_line_pattern = '^\([^:]\+\): \(.*\)$' 24 | 25 | function! s:get_lines(buffer, count, line1, line2) abort 26 | if a:count == -1 27 | return getbufline(a:buffer, 0, '$') 28 | else 29 | return getbufline(a:buffer, a:line1, a:line2) 30 | endif 31 | endfunction 32 | 33 | function! s:parse_request(lines, pattern, follow) abort 34 | let l:request = s:new_request() 35 | if a:follow == 1 36 | let l:request.meta.follow = 1 37 | end 38 | 39 | if len(a:lines) < 0 40 | throw 'No lines in buffer :(' 41 | endif 42 | 43 | let l:uri_line = a:lines[0] 44 | let l:uri_line_matches = matchlist(l:uri_line, a:pattern) 45 | 46 | if len(l:uri_line_matches) == 0 47 | throw 'Unable to parse first line of request' 48 | end 49 | 50 | let l:request.method = l:uri_line_matches[1] 51 | let l:request.uri = l:uri_line_matches[2] 52 | let l:request.version = l:uri_line_matches[3] 53 | 54 | if l:request.version =~ '2.*' 55 | let l:request.version = '2' 56 | end 57 | 58 | let l:in_content = 0 59 | let l:first_content_line = 1 60 | for l:line in a:lines[0:] 61 | if l:in_content == 0 && l:line =~ '^\s*$' 62 | let l:in_content = 1 63 | end 64 | if l:in_content == 0 65 | let l:header_matches = matchlist(l:line, s:header_line_pattern) 66 | 67 | if len(l:header_matches) > 0 68 | let l:header_key = l:header_matches[1] 69 | let l:header_value = l:header_matches[2] 70 | if has_key(l:request.headers, l:header_key) == 0 71 | let l:request.headers[l:header_key] = [] 72 | endif 73 | let l:request.headers[l:header_key] += [l:header_value] 74 | endif 75 | else 76 | if l:first_content_line && l:line =~ '^\s*$' 77 | continue 78 | endif 79 | if l:first_content_line 80 | let l:first_content_line = 0 81 | else 82 | let l:request.content .= "\r\n" 83 | endif 84 | let l:request.content .= l:line 85 | endif 86 | endfor 87 | 88 | return l:request 89 | endfunction 90 | 91 | function! s:in_curl_format(request) abort 92 | let l:curl_fmt = 'curl%s%s%s%s %s' 93 | 94 | let l:flags = ' -si' 95 | 96 | if a:request.meta.follow == 1 97 | let l:flags = l:flags . 'L' 98 | endif 99 | 100 | let l:flags = l:flags.' '.g:vim_http_additional_curl_args 101 | 102 | let l:flags = l:flags.' --http'.a:request.version 103 | 104 | let l:method = printf(' -X %s', a:request.method) 105 | 106 | let l:url = shellescape(a:request.uri) 107 | 108 | let l:headers = '' 109 | 110 | for [l:header_key, l:header_value_list] in items(a:request.headers) 111 | for l:header_value in l:header_value_list 112 | let l:headers = l:headers . 113 | \ ' -H "' . 114 | \ printf('%s: %s', l:header_key, l:header_value) . 115 | \ '"' 116 | endfor 117 | 118 | endfor 119 | 120 | let l:data = '' 121 | if len(a:request.content) 122 | let l:data = ' -d ' . substitute(shellescape(a:request.content), '\\\n', "\n", 'g') 123 | endif 124 | 125 | let l:curl_request = printf(l:curl_fmt, l:flags, l:method, l:headers, l:data, l:url) 126 | 127 | return l:curl_request 128 | 129 | endfunction 130 | 131 | function! s:lines_with_header(header) abort 132 | let l:linenr = 1 133 | let l:lines = [] 134 | while l:linenr < line('$') 135 | let l:line = getline(l:linenr) 136 | if l:line =~ '^\s*$' 137 | break 138 | endif 139 | if l:line =~ "^".a:header.":" 140 | call add(l:lines, l:linenr) 141 | end 142 | let l:linenr = l:linenr + 1 143 | endwhile 144 | return l:lines 145 | endfunction 146 | 147 | function! s:remove_header(header) abort 148 | let l:offset = 0 149 | for l:linenr in s:lines_with_header(a:header) 150 | let l:target = l:linenr - l:offset 151 | exe l:target."delete _" 152 | let l:offset = l:offset + 1 153 | endfor 154 | endfunction 155 | 156 | function! s:new_response_buffer(request_buffer, response) abort 157 | let l:request_buffer_name = bufname(a:request_buffer) 158 | let l:buffer_name = fnamemodify(l:request_buffer_name, ":r") . '.response.' . localtime() . '.http' 159 | if g:vim_http_tempbuffer 160 | for l:win in range(1, winnr('$')) 161 | if getwinvar(l:win, 'vim_http_tempbuffer') 162 | execute l:win . 'windo close' 163 | endif 164 | endfor 165 | endif 166 | let l:rightbelow = g:vim_http_right_below ? 'rightbelow ' : '' 167 | let l:keepalt = g:vim_http_tempbuffer ? 'keepalt ' : '' 168 | let l:vert = g:vim_http_split_vertically ? 'vert ' : '' 169 | execute l:keepalt . l:rightbelow . l:vert . 'new ' . l:buffer_name 170 | set ft=http 171 | if g:vim_http_tempbuffer 172 | setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile nonumber 173 | let w:vim_http_tempbuffer = 1 174 | endif 175 | 176 | let l:response_lines = split(a:response, "\\(\r\n\\|\n\\)") 177 | 178 | call append(0, l:response_lines) 179 | norm! G"_ddgg 180 | endfunction 181 | 182 | function! http#do_buffer(follow) abort 183 | let l:bang = a:follow ? '!' : '' 184 | call http#do(l:bang, -1, 0, 0) 185 | endfunction 186 | 187 | function! http#do(bang, count, line1, line2) abort 188 | let l:follow = a:bang == '!' ? 1 : 0 189 | if g:vim_http_clean_before_do && a:count == -1 190 | call http#clean() 191 | end 192 | let l:buffer = bufnr('') 193 | let l:lines = s:get_lines(l:buffer, a:count, a:line1, a:line2) 194 | let l:request = s:parse_request(l:lines, s:uri_line_pattern, l:follow) 195 | let l:curl = s:in_curl_format(l:request) 196 | let l:response = system(l:curl) 197 | call s:new_response_buffer(l:buffer, l:response) 198 | endfunction 199 | 200 | function! http#show_curl(bang, count, line1, line2) abort 201 | let l:follow = a:bang == '!' ? 1 : 0 202 | let l:buffer = bufnr('') 203 | let l:lines = s:get_lines(l:buffer, a:count, a:line1, a:line2) 204 | let l:request = s:parse_request(l:lines, s:uri_line_pattern, l:follow) 205 | let l:curl = s:in_curl_format(l:request) 206 | echo l:curl 207 | endfunction 208 | 209 | function! http#show_request(bang, count, line1, line2) abort 210 | let l:follow = a:bang == '!' ? 1 : 0 211 | let l:buffer = bufnr('') 212 | let l:lines = s:get_lines(l:buffer, a:count, a:line1, a:line2) 213 | let l:request = s:parse_request(l:lines, s:uri_line_pattern, l:follow) 214 | echo l:request 215 | endfunction 216 | 217 | function! http#clean() abort 218 | let l:buffer = bufnr('') 219 | let l:lines = s:get_lines(l:buffer, -1, 0, 0) 220 | let l:request = s:parse_request(l:lines, s:pre_clean_uri_line_pattern, 0) 221 | 222 | " when the http proto is > 1.0 make sure we are adding a host header 223 | if index(['1.1', '2'], l:request.version) != -1 && !has_key(l:request.headers, 'Host') 224 | let l:matches = matchlist(l:request.uri, '^\([^:]\+://\)\?\([^/]\+\)') 225 | let l:host = l:matches[2] 226 | if len(l:host) 227 | call append(1, 'Host: ' . l:host) 228 | endif 229 | endif 230 | 231 | if l:request.version == '' 232 | call setline('.', getline('.') . ' HTTP/1.1') 233 | endif 234 | 235 | let l:content_length = len(l:request.content) 236 | 237 | " when we have a Content-Length header and it doesn't match the actual 238 | " content length 239 | if l:content_length && has_key(l:request.headers, 'Content-Length') 240 | if string(l:content_length) != l:request.headers['Content-Length'][-1] 241 | let l:correct = input("correct Content-Length header? [Y]/N:") 242 | if len(l:correct) == 0 || tolower(l:correct) != "n" 243 | call remove(l:request.headers, 'Content-Length') 244 | call s:remove_header('Content-Length') 245 | endif 246 | endif 247 | endif 248 | 249 | " when we are sending content we should add a header for the content length 250 | " curl is going to do this for us anyway, but it's good to be explicit 251 | if l:content_length && !has_key(l:request.headers, 'Content-Length') 252 | call append(1 + len(l:request.headers), 'Content-Length: ' . l:content_length) 253 | endif 254 | endfunction 255 | 256 | function! http#auth() abort 257 | let l:buffer = bufnr('') 258 | let l:lines = s:get_lines(l:buffer, -1, 0, 0) 259 | let l:request = s:parse_request(l:lines, s:uri_line_pattern, 0) 260 | 261 | let l:method = input('method [Basic]: ') 262 | if len(l:method) == 0 263 | let l:method = 'Basic' 264 | end 265 | let l:user = input('user: ') 266 | let l:password = input('password: ') 267 | let l:encoded = s:Base64.encode(l:user . ':' . l:password) 268 | let l:header = 'Authorization: ' . l:method . ' ' . l:encoded 269 | call append(1 + len(l:request.headers), l:header) 270 | endfunction 271 | 272 | function! http#remove_header(header) abort 273 | call s:remove_header(a:header) 274 | endfunction 275 | 276 | function! http#set_header(header, value) abort 277 | call s:remove_header(a:header) 278 | let l:buffer = bufnr('') 279 | let l:lines = s:get_lines(l:buffer, -1, 0, 0) 280 | let l:request = s:parse_request(l:lines, s:uri_line_pattern, 0) 281 | call append(1 + len(l:request.headers), a:header.': '.a:value) 282 | endfunction 283 | 284 | " Teardown:{{{1 285 | let &cpo = s:save_cpo 286 | -------------------------------------------------------------------------------- /autoload/vital.vim: -------------------------------------------------------------------------------- 1 | function! vital#of(name) abort 2 | let files = globpath(&runtimepath, 'autoload/vital/' . a:name . '.vital', 1) 3 | let file = split(files, "\n") 4 | if empty(file) 5 | throw 'vital: version file not found: ' . a:name 6 | endif 7 | let ver = readfile(file[0], 'b') 8 | if empty(ver) 9 | throw 'vital: invalid version file: ' . a:name 10 | endif 11 | return vital#_{substitute(ver[0], '\W', '', 'g')}#new() 12 | endfunction 13 | -------------------------------------------------------------------------------- /autoload/vital/_vim_http.vim: -------------------------------------------------------------------------------- 1 | let s:_plugin_name = expand(':t:r') 2 | 3 | function! vital#{s:_plugin_name}#new() abort 4 | return vital#{s:_plugin_name[1:]}#new() 5 | endfunction 6 | -------------------------------------------------------------------------------- /autoload/vital/_vim_http/Data/Base64.vim: -------------------------------------------------------------------------------- 1 | " ___vital___ 2 | " NOTE: lines between '" ___vital___' is generated by :Vitalize. 3 | " Do not mofidify the code nor insert new lines before '" ___vital___' 4 | if v:version > 703 || v:version == 703 && has('patch1170') 5 | function! vital#_vim_http#Data#Base64#import() abort 6 | return map({'decode': '', 'encodebin': '', 'encode': ''}, 'function("s:" . v:key)') 7 | endfunction 8 | else 9 | function! s:_SID() abort 10 | return matchstr(expand(''), '\zs\d\+\ze__SID$') 11 | endfunction 12 | execute join(['function! vital#_vim_http#Data#Base64#import() abort', printf("return map({'decode': '', 'encodebin': '', 'encode': ''}, \"function('%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n") 13 | delfunction s:_SID 14 | endif 15 | " ___vital___ 16 | " Utilities for Base64. 17 | 18 | let s:save_cpo = &cpo 19 | set cpo&vim 20 | 21 | function! s:encode(data) abort 22 | let b64 = s:_b64encode(s:_str2bytes(a:data), s:standard_table, '=') 23 | return join(b64, '') 24 | endfunction 25 | 26 | function! s:encodebin(data) abort 27 | let b64 = s:_b64encode(s:_binstr2bytes(a:data), s:standard_table, '=') 28 | return join(b64, '') 29 | endfunction 30 | 31 | function! s:decode(data) abort 32 | let bytes = s:_b64decode(split(a:data, '\zs'), s:standard_table, '=') 33 | return s:_bytes2str(bytes) 34 | endfunction 35 | 36 | let s:standard_table = [ 37 | \ 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', 38 | \ 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', 39 | \ 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', 40 | \ 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'] 41 | 42 | let s:urlsafe_table = [ 43 | \ 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', 44 | \ 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', 45 | \ 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', 46 | \ 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','-','_'] 47 | 48 | function! s:_b64encode(bytes, table, pad) abort 49 | let b64 = [] 50 | for i in range(0, len(a:bytes) - 1, 3) 51 | let n = a:bytes[i] * 0x10000 52 | \ + get(a:bytes, i + 1, 0) * 0x100 53 | \ + get(a:bytes, i + 2, 0) 54 | call add(b64, a:table[n / 0x40000]) 55 | call add(b64, a:table[n / 0x1000 % 0x40]) 56 | call add(b64, a:table[n / 0x40 % 0x40]) 57 | call add(b64, a:table[n % 0x40]) 58 | endfor 59 | if len(a:bytes) % 3 == 1 60 | let b64[-1] = a:pad 61 | let b64[-2] = a:pad 62 | endif 63 | if len(a:bytes) % 3 == 2 64 | let b64[-1] = a:pad 65 | endif 66 | return b64 67 | endfunction 68 | 69 | function! s:_b64decode(b64, table, pad) abort 70 | let a2i = {} 71 | for i in range(len(a:table)) 72 | let a2i[a:table[i]] = i 73 | endfor 74 | let bytes = [] 75 | for i in range(0, len(a:b64) - 1, 4) 76 | let n = a2i[a:b64[i]] * 0x40000 77 | \ + a2i[a:b64[i + 1]] * 0x1000 78 | \ + (a:b64[i + 2] == a:pad ? 0 : a2i[a:b64[i + 2]]) * 0x40 79 | \ + (a:b64[i + 3] == a:pad ? 0 : a2i[a:b64[i + 3]]) 80 | call add(bytes, n / 0x10000) 81 | call add(bytes, n / 0x100 % 0x100) 82 | call add(bytes, n % 0x100) 83 | endfor 84 | if a:b64[-1] == a:pad 85 | unlet a:b64[-1] 86 | endif 87 | if a:b64[-2] == a:pad 88 | unlet a:b64[-1] 89 | endif 90 | return bytes 91 | endfunction 92 | 93 | function! s:_binstr2bytes(str) abort 94 | return map(range(len(a:str)/2), 'eval("0x".a:str[v:val*2 : v:val*2+1])') 95 | endfunction 96 | 97 | function! s:_str2bytes(str) abort 98 | return map(range(len(a:str)), 'char2nr(a:str[v:val])') 99 | endfunction 100 | 101 | function! s:_bytes2str(bytes) abort 102 | return eval('"' . join(map(copy(a:bytes), 'printf(''\x%02x'', v:val)'), '') . '"') 103 | endfunction 104 | 105 | let &cpo = s:save_cpo 106 | unlet s:save_cpo 107 | 108 | " vim:set et ts=2 sts=2 sw=2 tw=0: 109 | -------------------------------------------------------------------------------- /autoload/vital/vim_http.vim: -------------------------------------------------------------------------------- 1 | let s:plugin_name = expand(':t:r') 2 | let s:vital_base_dir = expand(':h') 3 | let s:project_root = expand(':h:h:h') 4 | let s:is_vital_vim = s:plugin_name is# 'vital' 5 | 6 | let s:loaded = {} 7 | let s:cache_sid = {} 8 | 9 | " function() wrapper 10 | if v:version > 703 || v:version == 703 && has('patch1170') 11 | function! s:_function(fstr) abort 12 | return function(a:fstr) 13 | endfunction 14 | else 15 | function! s:_SID() abort 16 | return matchstr(expand(''), '\zs\d\+\ze__SID$') 17 | endfunction 18 | let s:_s = '' . s:_SID() . '_' 19 | function! s:_function(fstr) abort 20 | return function(substitute(a:fstr, 's:', s:_s, 'g')) 21 | endfunction 22 | endif 23 | 24 | function! vital#{s:plugin_name}#new() abort 25 | return s:new(s:plugin_name) 26 | endfunction 27 | 28 | function! vital#{s:plugin_name}#import(...) abort 29 | if !exists('s:V') 30 | let s:V = s:new(s:plugin_name) 31 | endif 32 | return call(s:V.import, a:000, s:V) 33 | endfunction 34 | 35 | let s:Vital = {} 36 | 37 | function! s:new(plugin_name) abort 38 | let base = deepcopy(s:Vital) 39 | let base._plugin_name = a:plugin_name 40 | return base 41 | endfunction 42 | 43 | function! s:vital_files() abort 44 | if !exists('s:vital_files') 45 | let s:vital_files = map( 46 | \ s:is_vital_vim ? s:_global_vital_files() : s:_self_vital_files(), 47 | \ 'fnamemodify(v:val, ":p:gs?[\\\\/]?/?")') 48 | endif 49 | return copy(s:vital_files) 50 | endfunction 51 | let s:Vital.vital_files = s:_function('s:vital_files') 52 | 53 | function! s:import(name, ...) abort dict 54 | let target = {} 55 | let functions = [] 56 | for a in a:000 57 | if type(a) == type({}) 58 | let target = a 59 | elseif type(a) == type([]) 60 | let functions = a 61 | endif 62 | unlet a 63 | endfor 64 | let module = self._import(a:name) 65 | if empty(functions) 66 | call extend(target, module, 'keep') 67 | else 68 | for f in functions 69 | if has_key(module, f) && !has_key(target, f) 70 | let target[f] = module[f] 71 | endif 72 | endfor 73 | endif 74 | return target 75 | endfunction 76 | let s:Vital.import = s:_function('s:import') 77 | 78 | function! s:load(...) abort dict 79 | for arg in a:000 80 | let [name; as] = type(arg) == type([]) ? arg[: 1] : [arg, arg] 81 | let target = split(join(as, ''), '\W\+') 82 | let dict = self 83 | let dict_type = type({}) 84 | while !empty(target) 85 | let ns = remove(target, 0) 86 | if !has_key(dict, ns) 87 | let dict[ns] = {} 88 | endif 89 | if type(dict[ns]) == dict_type 90 | let dict = dict[ns] 91 | else 92 | unlet dict 93 | break 94 | endif 95 | endwhile 96 | if exists('dict') 97 | call extend(dict, self._import(name)) 98 | endif 99 | unlet arg 100 | endfor 101 | return self 102 | endfunction 103 | let s:Vital.load = s:_function('s:load') 104 | 105 | function! s:unload() abort dict 106 | let s:loaded = {} 107 | let s:cache_sid = {} 108 | unlet! s:vital_files 109 | endfunction 110 | let s:Vital.unload = s:_function('s:unload') 111 | 112 | function! s:exists(name) abort dict 113 | if a:name !~# '\v^\u\w*%(\.\u\w*)*$' 114 | throw 'vital: Invalid module name: ' . a:name 115 | endif 116 | return s:_module_path(a:name) isnot# '' 117 | endfunction 118 | let s:Vital.exists = s:_function('s:exists') 119 | 120 | function! s:search(pattern) abort dict 121 | let paths = s:_extract_files(a:pattern, self.vital_files()) 122 | let modules = sort(map(paths, 's:_file2module(v:val)')) 123 | return s:_uniq(modules) 124 | endfunction 125 | let s:Vital.search = s:_function('s:search') 126 | 127 | function! s:plugin_name() abort dict 128 | return self._plugin_name 129 | endfunction 130 | let s:Vital.plugin_name = s:_function('s:plugin_name') 131 | 132 | function! s:_self_vital_files() abort 133 | let builtin = printf('%s/__%s__/', s:vital_base_dir, s:plugin_name) 134 | let installed = printf('%s/_%s/', s:vital_base_dir, s:plugin_name) 135 | let base = builtin . ',' . installed 136 | return split(globpath(base, '**/*.vim', 1), "\n") 137 | endfunction 138 | 139 | function! s:_global_vital_files() abort 140 | let pattern = 'autoload/vital/__*__/**/*.vim' 141 | return split(globpath(&runtimepath, pattern, 1), "\n") 142 | endfunction 143 | 144 | function! s:_extract_files(pattern, files) abort 145 | let tr = {'.': '/', '*': '[^/]*', '**': '.*'} 146 | let target = substitute(a:pattern, '\.\|\*\*\?', '\=tr[submatch(0)]', 'g') 147 | let regexp = printf('autoload/vital/[^/]\+/%s.vim$', target) 148 | return filter(a:files, 'v:val =~# regexp') 149 | endfunction 150 | 151 | function! s:_file2module(file) abort 152 | let filename = fnamemodify(a:file, ':p:gs?[\\/]?/?') 153 | let tail = matchstr(filename, 'autoload/vital/_\w\+/\zs.*\ze\.vim$') 154 | return join(split(tail, '[\\/]\+'), '.') 155 | endfunction 156 | 157 | " @param {string} name e.g. Data.List 158 | function! s:_import(name) abort dict 159 | if has_key(s:loaded, a:name) 160 | return copy(s:loaded[a:name]) 161 | endif 162 | let module = self._get_module(a:name) 163 | if has_key(module, '_vital_created') 164 | call module._vital_created(module) 165 | endif 166 | let export_module = filter(copy(module), 'v:key =~# "^\\a"') 167 | " Cache module before calling module.vital_loaded() to avoid cyclic 168 | " dependences but remove the cache if module._vital_loaded() fails. 169 | " let s:loaded[a:name] = export_module 170 | let s:loaded[a:name] = export_module 171 | if has_key(module, '_vital_loaded') 172 | try 173 | call module._vital_loaded(vital#{s:plugin_name}#new()) 174 | catch 175 | unlet s:loaded[a:name] 176 | throw 'vital: fail to call ._vital_loaded(): ' . v:exception 177 | endtry 178 | endif 179 | return copy(s:loaded[a:name]) 180 | endfunction 181 | let s:Vital._import = s:_function('s:_import') 182 | 183 | " s:_get_module() returns module object wihch has all script local functions. 184 | function! s:_get_module(name) abort dict 185 | let funcname = s:_import_func_name(self.plugin_name(), a:name) 186 | if s:_exists_autoload_func_with_source(funcname) 187 | return call(funcname, []) 188 | else 189 | return s:_get_builtin_module(a:name) 190 | endif 191 | endfunction 192 | 193 | function! s:_get_builtin_module(name) abort 194 | return s:sid2sfuncs(s:_module_sid(a:name)) 195 | endfunction 196 | 197 | if s:is_vital_vim 198 | " For vital.vim, we can use s:_get_builtin_module directly 199 | let s:Vital._get_module = s:_function('s:_get_builtin_module') 200 | else 201 | let s:Vital._get_module = s:_function('s:_get_module') 202 | endif 203 | 204 | function! s:_import_func_name(plugin_name, module_name) abort 205 | return printf('vital#_%s#%s#import', a:plugin_name, s:_dot_to_sharp(a:module_name)) 206 | endfunction 207 | 208 | function! s:_module_sid(name) abort 209 | let path = s:_module_path(a:name) 210 | if !filereadable(path) 211 | throw 'vital: module not found: ' . a:name 212 | endif 213 | let vital_dir = s:is_vital_vim ? '__\w\+__' : printf('_\{1,2}%s\%%(__\)\?', s:plugin_name) 214 | let base = join([vital_dir, ''], '[/\\]\+') 215 | let p = base . substitute('' . a:name, '\.', '[/\\\\]\\+', 'g') 216 | let sid = s:_sid(path, p) 217 | if !sid 218 | call s:_source(path) 219 | let sid = s:_sid(path, p) 220 | if !sid 221 | throw printf('vital: cannot get from path: %s', path) 222 | endif 223 | endif 224 | return sid 225 | endfunction 226 | 227 | function! s:_module_path(name) abort 228 | return get(s:_extract_files(a:name, s:vital_files()), 0, '') 229 | endfunction 230 | 231 | function! s:_module_sid_base_dir() abort 232 | return s:is_vital_vim ? &rtp : s:project_root 233 | endfunction 234 | 235 | function! s:_dot_to_sharp(name) abort 236 | return substitute(a:name, '\.', '#', 'g') 237 | endfunction 238 | 239 | " It will sources autoload file if a given func is not already defined. 240 | function! s:_exists_autoload_func_with_source(funcname) abort 241 | if exists('*' . a:funcname) 242 | " Return true if a given func is already defined 243 | return 1 244 | endif 245 | " source a file which may include a given func definition and try again. 246 | let path = 'autoload/' . substitute(substitute(a:funcname, '#[^#]*$', '.vim', ''), '#', '/', 'g') 247 | call s:_runtime(path) 248 | return exists('*' . a:funcname) 249 | endfunction 250 | 251 | function! s:_runtime(path) abort 252 | execute 'runtime' fnameescape(a:path) 253 | endfunction 254 | 255 | function! s:_source(path) abort 256 | execute 'source' fnameescape(a:path) 257 | endfunction 258 | 259 | " @vimlint(EVL102, 1, l:_) 260 | " @vimlint(EVL102, 1, l:__) 261 | function! s:_sid(path, filter_pattern) abort 262 | let unified_path = s:_unify_path(a:path) 263 | if has_key(s:cache_sid, unified_path) 264 | return s:cache_sid[unified_path] 265 | endif 266 | for line in filter(split(s:_redir(':scriptnames'), "\n"), 'v:val =~# a:filter_pattern') 267 | let [_, sid, path; __] = matchlist(line, '^\s*\(\d\+\):\s\+\(.\+\)\s*$') 268 | if s:_unify_path(path) is# unified_path 269 | let s:cache_sid[unified_path] = sid 270 | return s:cache_sid[unified_path] 271 | endif 272 | endfor 273 | return 0 274 | endfunction 275 | 276 | function! s:_redir(cmd) abort 277 | let [save_verbose, save_verbosefile] = [&verbose, &verbosefile] 278 | set verbose=0 verbosefile= 279 | redir => res 280 | silent! execute a:cmd 281 | redir END 282 | let [&verbose, &verbosefile] = [save_verbose, save_verbosefile] 283 | return res 284 | endfunction 285 | 286 | if filereadable(expand(':r') . '.VIM') " is case-insensitive or not 287 | let s:_unify_path_cache = {} 288 | " resolve() is slow, so we cache results. 289 | " Note: On windows, vim can't expand path names from 8.3 formats. 290 | " So if getting full path via and $HOME was set as 8.3 format, 291 | " vital load duplicated scripts. Below's :~ avoid this issue. 292 | function! s:_unify_path(path) abort 293 | if has_key(s:_unify_path_cache, a:path) 294 | return s:_unify_path_cache[a:path] 295 | endif 296 | let value = tolower(fnamemodify(resolve(fnamemodify( 297 | \ a:path, ':p')), ':~:gs?[\\/]?/?')) 298 | let s:_unify_path_cache[a:path] = value 299 | return value 300 | endfunction 301 | else 302 | function! s:_unify_path(path) abort 303 | return resolve(fnamemodify(a:path, ':p:gs?[\\/]?/?')) 304 | endfunction 305 | endif 306 | 307 | " copied and modified from Vim.ScriptLocal 308 | let s:SNR = join(map(range(len("\")), '"[\\x" . printf("%0x", char2nr("\"[v:val])) . "]"'), '') 309 | function! s:sid2sfuncs(sid) abort 310 | let fs = split(s:_redir(printf(':function /^%s%s_', s:SNR, a:sid)), "\n") 311 | let r = {} 312 | let pattern = printf('\m^function\s%d_\zs\w\{-}\ze(', a:sid) 313 | for fname in map(fs, 'matchstr(v:val, pattern)') 314 | let r[fname] = function(s:_sfuncname(a:sid, fname)) 315 | endfor 316 | return r 317 | endfunction 318 | 319 | "" Return funcname of script local functions with SID 320 | function! s:_sfuncname(sid, funcname) abort 321 | return printf('%s_%s', a:sid, a:funcname) 322 | endfunction 323 | 324 | if exists('*uniq') 325 | function! s:_uniq(list) abort 326 | return uniq(a:list) 327 | endfunction 328 | else 329 | function! s:_uniq(list) abort 330 | let i = len(a:list) - 1 331 | while 0 < i 332 | if a:list[i] ==# a:list[i - 1] 333 | call remove(a:list, i) 334 | endif 335 | let i -= 1 336 | endwhile 337 | return a:list 338 | endfunction 339 | endif 340 | -------------------------------------------------------------------------------- /autoload/vital/vim_http.vital: -------------------------------------------------------------------------------- 1 | vim_http 2 | f07284453d294e2159424aa76b9b902a5b25d5ae 3 | 4 | Data.Base64 5 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | httpbin: 4 | image: citizenstig/httpbin 5 | oldest: 6 | image: thinca/vim:v7.4.566 7 | volumes: 8 | - .:/root/.vim 9 | latest: 10 | image: thinca/vim:latest 11 | volumes: 12 | - .:/root/.vim 13 | -------------------------------------------------------------------------------- /ftdetect/http.vim: -------------------------------------------------------------------------------- 1 | augroup filetypedetect 2 | au! BufRead,BufNewFile *.http setlocal filetype=http fileformat=dos 3 | augroup END 4 | -------------------------------------------------------------------------------- /plugin/http.vim: -------------------------------------------------------------------------------- 1 | if !exists('g:vim_http_clean_before_do') 2 | let g:vim_http_clean_before_do = 1 3 | endif 4 | if !exists('g:vim_http_additional_curl_args') 5 | let g:vim_http_additional_curl_args = '' 6 | endif 7 | if !exists('g:vim_http_split_vertically') 8 | let g:vim_http_split_vertically = 0 9 | endif 10 | if !exists('g:vim_http_right_below') 11 | let g:vim_http_right_below = 0 12 | endif 13 | if !exists('g:vim_http_tempbuffer') 14 | let g:vim_http_tempbuffer = 0 15 | endif 16 | 17 | command! -bang -range Http call http#do('', '', '', '') 18 | command! -bang -range HttpShowCurl call http#show_curl('', '', '', '') 19 | command! -bang -range HttpShowRequest call http#show_request('', '', '', '') 20 | command! HttpClean call http#clean() 21 | command! HttpAuth call http#auth() 22 | -------------------------------------------------------------------------------- /syntax/http.vim: -------------------------------------------------------------------------------- 1 | if version < 600 2 | syntax clear 3 | elseif exists("b:current_syntax") 4 | finish 5 | endif 6 | 7 | let s:cpo_save = &cpo 8 | set cpo&vim 9 | 10 | syn keyword httpMethod OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT PATCH contained 11 | 12 | syn match httpProto 'HTTP/[0-9.]\+' contained 13 | syn match httpStatusCode '[0-9]\{3\}' contained 14 | syn match httpStatus '[0-9]\{3\} .*$' contained contains=httpStatusCode 15 | syn match httpHeaderKey '^[A-Z][A-Za-z0-9\-]*:' contained 16 | syn match httpURILine '^\(OPTIONS\|GET\|HEAD\|POST\|PUT\|DELETE\|TRACE\|CONNECT\|PATCH\)\( .*\)\?\(HTTP/[0-9.]\+\)\?$' contains=httpMethod,httpProto contained 17 | syn match httpResponseLine '^HTTP/[0-9.]\+ [0-9]\{3\}.*$' contains=httpProto,httpStatus contained 18 | syn match httpHeaderLine '^[A-Z][A-Za-z0-9\-]*: .*$' contains=httpHeaderKey contained 19 | 20 | syn region httpHeader start='^\(OPTIONS\|GET\|HEAD\|POST\|PUT\|DELETE\|TRACE\|CONNECT\|PATCH\)\( .*\)\?\(HTTP/[0-9.]\+\)\?$' end='\n\s*\n' contains=httpURILine,httpHeaderLine 21 | syn region httpHeader start='^HTTP/[0-9.]\+ [0-9]\{3\}.*$' end='\n\s*\n' contains=httpResponseLine,httpHeaderLine 22 | 23 | hi link httpMethod Type 24 | hi link httpProto Statement 25 | hi link httpHeaderKey Identifier 26 | hi link httpStatus String 27 | hi link httpStatusCode Number 28 | 29 | let b:current_syntax = 'http' 30 | 31 | let &cpo = s:cpo_save 32 | unlet s:cpo_save 33 | -------------------------------------------------------------------------------- /test/.themisrc: -------------------------------------------------------------------------------- 1 | 2 | let g:http_test_files = expand(':p:h') . '/test-http-files/' 3 | 4 | call themis#log('test_files_location: ' . g:http_test_files) 5 | -------------------------------------------------------------------------------- /test/integration.vim: -------------------------------------------------------------------------------- 1 | let s:suite = themis#suite('integration') 2 | let s:assert = themis#helper('assert') 3 | 4 | " Setup: {{{1 5 | let s:original_buffers = filter(range(1, bufnr('$')), 'bufexists(v:val)') 6 | 7 | function! s:suite.before_each() 8 | let l:current_buffers = filter(range(1, bufnr('$')), 'bufexists(v:val)') 9 | let l:buffers_to_wipe = filter(copy(l:current_buffers), 'index(s:original_buffers, v:val) == -1') 10 | for l:buffer in l:buffers_to_wipe 11 | execute 'bw!' . l:buffer 12 | endfor 13 | endfunction 14 | 15 | function! s:load_request_expected(name) abort 16 | let l:request_path = g:http_test_files . a:name . '.http' 17 | execute 'edit! ' . l:request_path 18 | endfunction 19 | 20 | function! s:load_request_expected_explicit(name) abort 21 | let l:request_path = g:http_test_files . a:name 22 | execute 'edit! ' . l:request_path 23 | endfunction 24 | 25 | function s:assert_response_explicit(filepath, name) abort 26 | let l:expected_path = g:http_test_files . a:filepath 27 | let l:expected = readfile(l:expected_path) 28 | let l:lines = getbufline(a:name, 0, '$') 29 | 30 | let l:bad = 0 31 | let l:lnr = 0 32 | for l:expect in l:expected 33 | let l:line = l:lines[l:lnr] 34 | if l:expect =~ '.*%%[^%]\+%%.*' 35 | let l:patterns = split(l:expect, '%%') 36 | let l:in_pattern = 0 37 | let l:line_index = 0 38 | let l:pattern_nr = 0 39 | for l:pattern in l:patterns 40 | if l:in_pattern 41 | let l:p = '\(' . substitute(l:pattern, '^\s*\([''"]\)\(.*\)\1\s*$', '\2', '') . '\)' 42 | let l:pp = l:p 43 | if len(l:patterns) > l:pattern_nr+1 44 | let l:pp = l:p . l:patterns[l:pattern_nr+1] 45 | endif 46 | if l:line[l:line_index :] !~ l:pp 47 | let l:bad = 1 48 | break 49 | endif 50 | let l:line_index += len(substitute(l:line[l:line_index :], l:pp, '\1', '')) 51 | let l:in_pattern = 0 52 | else 53 | if l:pattern != l:line[l:line_index : l:line_index + len(l:pattern)-1] 54 | let l:bad = 1 55 | break 56 | end 57 | let l:line_index += len(l:pattern) 58 | let l:in_pattern = 1 59 | endif 60 | 61 | let l:pattern_nr += 1 62 | endfor 63 | else 64 | if l:line != l:expect 65 | let l:bad = 1 66 | endif 67 | endif 68 | if l:bad == 1 69 | break 70 | end 71 | 72 | let l:lnr += 1 73 | 74 | endfor 75 | 76 | if l:bad 77 | let l:msg = [printf('Got response for %s.http:', a:name)] 78 | let l:msg = l:msg + [''] + l:lines + ['', 'Expected:'] + l:expected 79 | throw themis#failure(msg) 80 | endif 81 | endfunction 82 | 83 | function s:assert_response(name) abort 84 | call s:assert_response_explicit(a:name . '.expected.http', a:name . '.response.*.http') 85 | endfunction 86 | 87 | function! s:command_with_input(cmd, input) 88 | exe 'normal :' . a:cmd .' '.join(a:input, ' ').' ' 89 | endfunction 90 | " }}} 91 | " GET: {{{1 92 | function! s:suite.simple_get() 93 | call s:load_request_expected('simple_get') 94 | Http 95 | call s:assert_response('simple_get') 96 | endfunction 97 | 98 | function! s:suite.get_with_url_params() 99 | call s:load_request_expected('get_url_params') 100 | Http 101 | call s:assert_response('get_url_params') 102 | endfunction 103 | 104 | function! s:suite.get_with_body_params() 105 | call s:load_request_expected('get_body_params') 106 | Http 107 | call s:assert_response('get_body_params') 108 | endfunction 109 | 110 | function! s:suite.http_1_0() 111 | call s:load_request_expected('http_1_0') 112 | Http! 113 | call s:assert_response('http_1_0') 114 | endfunction 115 | 116 | function! s:suite.http_1_1() 117 | call s:load_request_expected('http_1_1') 118 | Http! 119 | call s:assert_response('http_1_1') 120 | endfunction 121 | 122 | function! s:suite.redirect() 123 | call s:load_request_expected('redirect') 124 | Http 125 | call s:assert_response('redirect') 126 | endfunction 127 | 128 | function! s:suite.redirect_with_follow() 129 | call s:load_request_expected('redirect_follow') 130 | Http! 131 | call s:assert_response('redirect_follow') 132 | endfunction 133 | 134 | " }}} 135 | " POST :{{{1 136 | function! s:suite.post_json() 137 | call s:load_request_expected('post_json') 138 | Http! 139 | call s:assert_response('post_json') 140 | endfunction 141 | 142 | " }}} 143 | " PATCH :{{{1 144 | function! s:suite.patch_json() 145 | call s:load_request_expected('patch_json') 146 | Http! 147 | call s:assert_response('patch_json') 148 | endfunction 149 | 150 | " }}} 151 | " Clean :{{{1 152 | function! s:suite.clean() 153 | call s:load_request_expected('post_incomplete') 154 | HttpClean 155 | let l:contents = getline(0, '$') 156 | let l:expected = ['POST http://localhost:8000/post HTTP/1.1', 157 | \ 'Host: localhost:8000', 158 | \ 'Content-Length: 43', 159 | \ 'Content-Type: application/json', 160 | \ '', 161 | \ '', 162 | \ '{', 163 | \ ' "foo": "bar",', 164 | \ ' "lol": "beans"', 165 | \ '}', 166 | \ ] 167 | call s:assert.equal(l:contents, l:expected) 168 | endfunction 169 | 170 | function! s:suite.clean_adds_host_on_http2_requests() 171 | call s:load_request_expected('http_2') 172 | HttpClean 173 | let l:contents = getline(0, '$') 174 | let l:expected = ['GET http://localhost:8000/get HTTP/2', 175 | \ 'Host: localhost:8000', 176 | \ ] 177 | call s:assert.equal(l:contents, l:expected) 178 | endfunction 179 | 180 | function! s:suite.clean_replaces_invalid_content_length_header() 181 | call s:load_request_expected('post_invalid_content_length') 182 | call s:command_with_input('HttpClean', ['Y']) 183 | let l:contents = getline(0, '$') 184 | let l:expected = ['POST http://localhost:8000/post HTTP/1.1', 185 | \ 'Host: localhost:8000', 186 | \ 'Content-Type: application/json', 187 | \ 'Content-Length: 43', 188 | \ '', 189 | \ '', 190 | \ '{', 191 | \ ' "foo": "bar",', 192 | \ ' "lol": "beans"', 193 | \ '}', 194 | \ ] 195 | call s:assert.equal(l:contents, l:expected) 196 | endfunction 197 | 198 | function! s:suite.clean_appends_protocol() 199 | call s:load_request_expected('simple_get_without_protocol') 200 | call s:command_with_input('HttpClean', ['Y']) 201 | let l:contents = getline(0, '$') 202 | let l:expected = ['GET http://localhost:8000/get HTTP/1.1'] 203 | call s:assert.equal(l:contents, l:expected) 204 | endfunction 205 | 206 | " }}} 207 | " Auth: {{{1 208 | function! s:suite.auth() 209 | call s:load_request_expected('simple_get') 210 | call s:command_with_input('HttpAuth', ['Bearer', 'borisjohnson', 'ijustcantwaittobeking']) 211 | let l:contents = getline(0, '$') 212 | let l:expected = ['GET http://localhost:8000/get HTTP/1.1', 213 | \ 'Host: localhost:8000', 214 | \ 'Authorization: Bearer Ym9yaXNqb2huc29uOmlqdXN0Y2FudHdhaXR0b2Jla2luZw==', 215 | \ ] 216 | call s:assert.equal(l:contents, l:expected) 217 | endfunction 218 | " }}} 219 | " Headers: {{{1 220 | function! s:suite.remove_header() 221 | call s:load_request_expected('get_with_multiple_headers') 222 | call http#remove_header('User-Agent') 223 | let l:contents = getline(0, '$') 224 | let l:expected = ['GET http://localhost:8000/get HTTP/1.1', 225 | \ 'Host: localhost:8000', 226 | \ 'Accept: text/html', 227 | \ 'Accept-Encoding: gzip, deflate', 228 | \ 'Accept-Language: en-US,en;q=0.5 ', 229 | \ 'Connection: close', 230 | \ 'Upgrade-Insecure-Requests: 1 ', 231 | \ ] 232 | call s:assert.equal(l:contents, l:expected) 233 | endfunction 234 | 235 | function! s:suite.set_header() 236 | call s:load_request_expected('get_with_multiple_headers') 237 | call http#set_header('User-Agent', 'qutebrowser/1.0.0') 238 | let l:contents = getline(0, '$') 239 | let l:expected = ['GET http://localhost:8000/get HTTP/1.1', 240 | \ 'Host: localhost:8000', 241 | \ 'Accept: text/html', 242 | \ 'Accept-Encoding: gzip, deflate', 243 | \ 'Accept-Language: en-US,en;q=0.5 ', 244 | \ 'Connection: close', 245 | \ 'Upgrade-Insecure-Requests: 1 ', 246 | \ 'User-Agent: qutebrowser/1.0.0', 247 | \ ] 248 | call s:assert.equal(l:contents, l:expected) 249 | endfunction 250 | " }}} 251 | " Misc: {{{1 252 | function! s:suite.tempbuffer() 253 | let l:vim_http_tempbuffer = g:vim_http_tempbuffer 254 | let g:vim_http_tempbuffer = 1 255 | 256 | call s:load_request_expected('simple_get') 257 | call s:load_request_expected('get_url_params') 258 | let l:expected = filter(range(1, bufnr('$')), 'bufexists(v:val)') 259 | 260 | Http " test get_url_params.http 261 | wincmd p " switch from response window back to get_url_params.http window 262 | buffer # " switch to simple_get.http buffer 263 | Http " test simple_get.http 264 | wincmd c " close response window 265 | 266 | let l:contents = filter(range(1, bufnr('$')), 'bufexists(v:val)') 267 | let g:vim_http_tempbuffer = l:vim_http_tempbuffer 268 | call s:assert.equal(l:contents, l:expected) 269 | endfunction 270 | 271 | " Range: {{{1 272 | 273 | function! s:suite.do_with_range() 274 | call s:load_request_expected_explicit('example_in_docs.md') 275 | 14,15Http! 276 | call s:assert_response_explicit('simple_get.expected.http', 'example_in_docs.response.*.http') 277 | endfunction 278 | 279 | " vim:fdm=marker 280 | -------------------------------------------------------------------------------- /test/test-http-files/example_in_docs.md: -------------------------------------------------------------------------------- 1 | Imagine this was some documentation and we had an HTTP request right there in 2 | the file. Why do I have to create a new buffer for it? That's just dumb lol! 3 | 4 | ```http 5 | POST http://localhost:8000/post HTTP/1.1 6 | Host: "localhost:8000" 7 | 8 | I AM A READMEFILE HERE ME ROAR! 9 | ``` 10 | 11 | So let's execute this and see what happens: 12 | 13 | ```http 14 | GET http://localhost:8000/get HTTP/1.1 15 | Host: localhost:8000 16 | ``` 17 | 18 | Did it work? 19 | 20 | ```http 21 | PATCH http://localhost:8000/patch HTTP/1.1 22 | Host: "localhost:8000" 23 | 24 | LOLOLOLOLOLOL 25 | ``` 26 | 27 | I hope so 28 | 29 | Here is a picture of a cat: 30 | 31 | ![CAT!](https://i.imgur.com/DKUR9Tk.png) 32 | -------------------------------------------------------------------------------- /test/test-http-files/get_body_params.expected.http: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: gunicorn/%% '.*' %% 3 | Date: %% '.*' %% 4 | Connection: close 5 | Content-Type: application/json 6 | Access-Control-Allow-Origin: * 7 | Access-Control-Allow-Credentials: true 8 | Content-Length: %% '[0-9]\+' %% 9 | 10 | { 11 | "args": {}, 12 | "headers": { 13 | "Accept": "*/*", 14 | "Content-Length": "17", 15 | "Content-Type": "application/x-www-form-urlencoded", 16 | "Host": "localhost:8000", 17 | "User-Agent": "curl/%% '.*' %%" 18 | }, 19 | "origin": "%% '[0-9.]\+' %%", 20 | "url": "http://localhost:8000/get" 21 | } 22 | -------------------------------------------------------------------------------- /test/test-http-files/get_body_params.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:8000/get HTTP/1.1 2 | Host: localhost:8000 3 | 4 | 5 | foo=bar&lol=beans 6 | -------------------------------------------------------------------------------- /test/test-http-files/get_url_params.expected.http: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: gunicorn/%% '.*' %% 3 | Date: %% '.*' %% 4 | Connection: close 5 | Content-Type: application/json 6 | Access-Control-Allow-Origin: * 7 | Access-Control-Allow-Credentials: true 8 | Content-Length: %% '[0-9]\+' %% 9 | 10 | { 11 | "args": { 12 | "foo": "bar", 13 | "lol": "beans" 14 | }, 15 | "headers": { 16 | "Accept": "*/*", 17 | "Host": "localhost:8000", 18 | "User-Agent": "curl/%% '.*' %%" 19 | }, 20 | "origin": "%% '[0-9.]\+' %%", 21 | "url": "http://localhost:8000/get?foo=bar&lol=beans" 22 | } 23 | -------------------------------------------------------------------------------- /test/test-http-files/get_url_params.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:8000/get?foo=bar&lol=beans HTTP/1.1 2 | Host: localhost:8000 3 | -------------------------------------------------------------------------------- /test/test-http-files/get_with_multiple_headers.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:8000/get HTTP/1.1 2 | Host: localhost:8000 3 | Accept: text/html 4 | Accept-Encoding: gzip, deflate 5 | User-Agent: Mozilla/5.0 6 | Accept-Language: en-US,en;q=0.5 7 | Connection: close 8 | User-Agent: Mozilla/4.0 9 | Upgrade-Insecure-Requests: 1 10 | -------------------------------------------------------------------------------- /test/test-http-files/http_1_0.expected.http: -------------------------------------------------------------------------------- 1 | HTTP/1.0 200 OK 2 | Server: gunicorn/%% '.*' %% 3 | Date: %% '.*' %% 4 | Connection: close 5 | Content-Type: application/json 6 | Access-Control-Allow-Origin: * 7 | Access-Control-Allow-Credentials: true 8 | Content-Length: %% '[0-9]\+' %% 9 | 10 | { 11 | "args": {}, 12 | "headers": { 13 | "Accept": "*/*", 14 | "Host": "localhost:8000", 15 | "User-Agent": "curl/%% '.*' %%" 16 | }, 17 | "origin": "%% '[0-9.]\+' %%", 18 | "url": "http://localhost:8000/get" 19 | } 20 | -------------------------------------------------------------------------------- /test/test-http-files/http_1_0.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:8000/get HTTP/1.0 2 | Host: localhost:8000 3 | -------------------------------------------------------------------------------- /test/test-http-files/http_1_1.expected.http: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: gunicorn/%% '.*' %% 3 | Date: %% '.*' %% 4 | Connection: close 5 | Content-Type: application/json 6 | Access-Control-Allow-Origin: * 7 | Access-Control-Allow-Credentials: true 8 | Content-Length: %% '[0-9]\+' %% 9 | 10 | { 11 | "args": {}, 12 | "headers": { 13 | "Accept": "*/*", 14 | "Host": "localhost:8000", 15 | "User-Agent": "curl/%% '.*' %%" 16 | }, 17 | "origin": "%% '[0-9.]\+' %%", 18 | "url": "http://localhost:8000/get" 19 | } 20 | -------------------------------------------------------------------------------- /test/test-http-files/http_1_1.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:8000/get HTTP/1.1 2 | Host: localhost:8000 3 | -------------------------------------------------------------------------------- /test/test-http-files/http_2.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:8000/get HTTP/2 2 | -------------------------------------------------------------------------------- /test/test-http-files/patch_json.expected.http: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: gunicorn/%% '.*' %% 3 | Date: %% '.*' %% 4 | Connection: close 5 | Content-Type: application/json 6 | Access-Control-Allow-Origin: * 7 | Access-Control-Allow-Credentials: true 8 | Content-Length: %% '[0-9]\+' %% 9 | 10 | { 11 | "args": {}, 12 | "data": "{\r\n \"foo\": \"bar\",\r\n \"lol\": \"beans\"\r\n}", 13 | "files": {}, 14 | "form": {}, 15 | "headers": { 16 | "Accept": "*/*", 17 | "Content-Length": "43", 18 | "Content-Type": "application/json", 19 | "Host": "localhost:8000", 20 | "User-Agent": "curl/%% '.*' %%" 21 | }, 22 | "json": { 23 | "foo": "bar", 24 | "lol": "beans" 25 | }, 26 | "origin": "%% '[0-9.]\+' %%", 27 | "url": "http://localhost:8000/patch" 28 | } 29 | -------------------------------------------------------------------------------- /test/test-http-files/patch_json.http: -------------------------------------------------------------------------------- 1 | PATCH http://localhost:8000/patch HTTP/1.1 2 | Host: localhost:8000 3 | Content-Type: application/json 4 | Content-Length: 43 5 | 6 | 7 | { 8 | "foo": "bar", 9 | "lol": "beans" 10 | } 11 | -------------------------------------------------------------------------------- /test/test-http-files/post_incomplete.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:8000/post HTTP/1.1 2 | Content-Type: application/json 3 | 4 | 5 | { 6 | "foo": "bar", 7 | "lol": "beans" 8 | } 9 | -------------------------------------------------------------------------------- /test/test-http-files/post_invalid_content_length.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:8000/post HTTP/1.1 2 | Host: localhost:8000 3 | Content-Length: 20 4 | Content-Type: application/json 5 | 6 | 7 | { 8 | "foo": "bar", 9 | "lol": "beans" 10 | } 11 | -------------------------------------------------------------------------------- /test/test-http-files/post_json.expected.http: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: gunicorn/%% '.*' %% 3 | Date: %% '.*' %% 4 | Connection: close 5 | Content-Type: application/json 6 | Access-Control-Allow-Origin: * 7 | Access-Control-Allow-Credentials: true 8 | Content-Length: %% '[0-9]\+' %% 9 | 10 | { 11 | "args": {}, 12 | "data": "{\r\n \"foo\": \"bar\",\r\n \"lol\": \"beans\"\r\n}", 13 | "files": {}, 14 | "form": {}, 15 | "headers": { 16 | "Accept": "*/*", 17 | "Content-Length": "43", 18 | "Content-Type": "application/json", 19 | "Host": "localhost:8000", 20 | "User-Agent": "curl/%% '.*' %%" 21 | }, 22 | "json": { 23 | "foo": "bar", 24 | "lol": "beans" 25 | }, 26 | "origin": "%% '[0-9.]\+' %%", 27 | "url": "http://localhost:8000/post" 28 | } 29 | -------------------------------------------------------------------------------- /test/test-http-files/post_json.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:8000/post HTTP/1.1 2 | Host: "localhost:8000" 3 | Content-Type: application/json 4 | Content-Length: 43 5 | 6 | 7 | { 8 | "foo": "bar", 9 | "lol": "beans" 10 | } 11 | -------------------------------------------------------------------------------- /test/test-http-files/redirect.expected.http: -------------------------------------------------------------------------------- 1 | HTTP/1.1 302 FOUND 2 | Server: gunicorn/%% '.*' %% 3 | Date: %% '.*' %% 4 | Connection: close 5 | Content-Type: text/html; charset=utf-8 6 | Content-Length: 0 7 | Location: http://localhost:8000/get 8 | Access-Control-Allow-Origin: * 9 | Access-Control-Allow-Credentials: true 10 | 11 | -------------------------------------------------------------------------------- /test/test-http-files/redirect.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:8000/redirect-to?url=http://localhost:8000/get HTTP/1.1 2 | Host: localhost:8000 3 | -------------------------------------------------------------------------------- /test/test-http-files/redirect_follow.expected.http: -------------------------------------------------------------------------------- 1 | HTTP/1.1 302 FOUND 2 | Server: gunicorn/%% '.*' %% 3 | Date: %% '.*' %% 4 | Connection: close 5 | Content-Type: text/html; charset=utf-8 6 | Content-Length: 0 7 | Location: http://localhost:8000/get 8 | Access-Control-Allow-Origin: * 9 | Access-Control-Allow-Credentials: true 10 | 11 | HTTP/1.1 200 OK 12 | Server: gunicorn/%% '.*' %% 13 | Date: %% '.*' %% 14 | Connection: close 15 | Content-Type: application/json 16 | Access-Control-Allow-Origin: * 17 | Access-Control-Allow-Credentials: true 18 | Content-Length: %% '[0-9]\+' %% 19 | 20 | { 21 | "args": {}, 22 | "headers": { 23 | "Accept": "*/*", 24 | "Host": "localhost:8000", 25 | "User-Agent": "curl/%% '.*' %%" 26 | }, 27 | "origin": "%% '[0-9.]\+' %%", 28 | "url": "http://localhost:8000/get" 29 | } 30 | -------------------------------------------------------------------------------- /test/test-http-files/redirect_follow.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:8000/redirect-to?url=http://localhost:8000/get HTTP/1.1 2 | Host: localhost:8000 3 | -------------------------------------------------------------------------------- /test/test-http-files/simple_get.expected.http: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: gunicorn/%% '.*' %% 3 | Date: %% '.*' %% 4 | Connection: close 5 | Content-Type: application/json 6 | Access-Control-Allow-Origin: * 7 | Access-Control-Allow-Credentials: true 8 | Content-Length: %% '[0-9]\+' %% 9 | 10 | { 11 | "args": {}, 12 | "headers": { 13 | "Accept": "*/*", 14 | "Host": "localhost:8000", 15 | "User-Agent": "curl/%% '.*' %%" 16 | }, 17 | "origin": "%% '[0-9.]\+' %%", 18 | "url": "http://localhost:8000/get" 19 | } 20 | -------------------------------------------------------------------------------- /test/test-http-files/simple_get.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:8000/get HTTP/1.1 2 | Host: localhost:8000 3 | -------------------------------------------------------------------------------- /test/test-http-files/simple_get_without_protocol.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:8000/get 2 | --------------------------------------------------------------------------------