├── .gitignore ├── LICENSE.md ├── README.md ├── autoload └── mdnquery.vim ├── doc └── mdnquery.txt ├── plugin └── mdnquery.vim └── screenshots └── demo.gif /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2016 Michael Jungo 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vim-MdnQuery 2 | 3 | Query the [Mozilla Developer Network][mdn] documentation without leaving Vim. 4 | The network requests are done asynchronously if the job-control feature is 5 | available (both in NeoVim and Vim), otherwise it falls back to using Ruby. 6 | To avoid unnecessary requests, the search results and documentation entries are 7 | cached in the current Vim instance, which allows to switch quickly between them. 8 | 9 | ![Demo][demo] 10 | 11 | ## Requirements 12 | 13 | - NeoVim or Vim with the job-control feature for asynchronous execution. 14 | - Vim compiled with Ruby support when job-control is not available. 15 | - The gem [mdn_query][mdn_query]. 16 | 17 | ## Installation 18 | 19 | ```sh 20 | gem install mdn_query 21 | ``` 22 | 23 | Install the plugin with your favourite plugin manager. 24 | 25 | ### Example using [vim-plug][vim-plug] 26 | 27 | Add the following to `~/.vimrc` or `~/.config/nvim/init.vim` respectively: 28 | 29 | ```vim 30 | Plug 'jungomi/vim-mdnquery' 31 | ``` 32 | 33 | Reload the config file and install the plugins: 34 | 35 | ``` 36 | :source $MYVIMRC 37 | :PlugInstall 38 | ``` 39 | 40 | ## Usage 41 | 42 | ### Simple Search 43 | 44 | ``` 45 | :MdnQuery array remove 46 | ``` 47 | 48 | Searches for `array remove` and shows the list of results in a buffer when the 49 | search finishes. Inside the buffer you can open the entry under the cursor by 50 | pressing ``. When showing an entry you can press `r` to return to the 51 | list of results. 52 | 53 | Often a search query is specific enough that the first result in the list is the 54 | one that will be opened. Doing that manually would quickly become annoying and 55 | for this reason `:MdnQueryFirstMatch` exists, which automatically opens the 56 | first entry. 57 | 58 | ``` 59 | :MdnQueryFirstMatch array.pop 60 | ``` 61 | 62 | ### Keywordprg (K command) 63 | 64 | The K command is used to lookup documentation for the word under the cursor. It 65 | defaults to `man` on Unix and `:help` otherwise. The default behaviour is not 66 | very useful for many file types. This plugin automatically changes that for 67 | JavaScript files . Pressing K in normal mode uses this plugin to search for the 68 | word under the cursor. 69 | 70 | It might be useful to also have this behaviour for other file types, so you can 71 | use a simple autocommand to set it for them: 72 | 73 | ```vim 74 | autocmd FileType html setlocal keywordprg=:MdnQueryFirstMatch 75 | ``` 76 | 77 | *See `:help mdnquery-keyworprg` for more details.* 78 | 79 | ### Topics 80 | 81 | The search is limited to the topics specified in `g:mdnquery_topics`, which is 82 | a list of topics and defaults to `['js']`. Having a global list of topics for 83 | all searches might show some undesired results. Instead of having to change the 84 | global option, you can set `b:mdnquery_topics`, which is local to the current 85 | buffer and is used over the global one if it exists. This can easily be combined 86 | with an autocommand to set the correct topics for a specific file type. 87 | 88 | ```vim 89 | " Search in JS and CSS topics 90 | let g:mdnquery_topics = ['js', 'css'] 91 | " Search only for HTML in the current buffer 92 | let b:mdnquery_topics = ['html'] 93 | 94 | " Automatically set the topics for HTML files 95 | autocmd FileType html let b:mdnquery_topics = ['css', 'html'] 96 | ``` 97 | 98 | If you would like to execute a search for specific topics without having to 99 | change any settings, you can use the functions `mdnquery#search(query, topics)` 100 | and `mdnquery#firstMatch(query, topics)`. 101 | 102 | ```vim 103 | call mdnquery#search('link', ['css', 'html']) 104 | call mdnquery#firstMatch('flex align', ['css']) 105 | ``` 106 | 107 | ### Buffer appearance 108 | 109 | By default the buffer appears after a search is completed and it is not 110 | automatically focused. You can change this behaviour by changing the 111 | `g:mdnquery_show_on_invoke` and `g:mdnquery_auto_focus` settings. The buffer is 112 | opened with the `:botright` command and therefore appears at full width on the 113 | bottom of the screen or when `g:mdnquery_vertical` is set, it appears at full 114 | height on the very right of the screen. The size of the buffer can be changed 115 | with the `g:mdnquery_size` setting. For example to automatically show and focus 116 | the window with a height of 10 lines, this configuration can be used: 117 | 118 | ```vim 119 | let g:mdnquery_show_on_invoke = 1 120 | let g:mdnquery_auto_focus = 1 121 | let g:mdnquery_size = 10 122 | ``` 123 | 124 | If you prefer to only focus the buffer when a search is finished, you can use 125 | the following autocommand instead of setting `g:mdnquery_auto_focus`: 126 | 127 | ```vim 128 | autocmd User MdnQueryContentChange call mdnquery#focus() 129 | ``` 130 | 131 | *See `:help mdnquery-settings` for the full list of settings.* 132 | 133 | ### Documentation 134 | 135 | For additional and more detailed information take a look at the plugin's help. 136 | 137 | ```vim 138 | :help mdnquery.txt 139 | ``` 140 | 141 | ## Known Issues 142 | 143 | *Only for Vim versions without the job-control feature.* 144 | 145 | `LoadError: incompatible library version - Nokogiri` 146 | 147 | This error occurs when using a Ruby installed with RVM but Vim was compiled with 148 | system ruby. To fix it tell RVM to use system Ruby and then reinstall the gem, 149 | or simply get a Vim version with job-control support. 150 | 151 | ```sh 152 | rvm use system 153 | gem install mdn_query 154 | ``` 155 | 156 | [demo]: screenshots/demo.gif 157 | [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript 158 | [mdn_query]: https://github.com/jungomi/mdn_query 159 | [vim-plug]: https://github.com/junegunn/vim-plug 160 | -------------------------------------------------------------------------------- /autoload/mdnquery.vim: -------------------------------------------------------------------------------- 1 | function! s:hasJob() abort 2 | return has('nvim') || has('job') && has('channel') 3 | endfunction 4 | 5 | if !s:hasJob() 6 | ruby require 'mdn_query' 7 | endif 8 | 9 | function! s:msg(msg) abort 10 | echomsg 'MdnQuery: ' . a:msg 11 | endfunction 12 | 13 | function! s:errorMsg(msg) abort 14 | echohl ErrorMsg 15 | echomsg 'MdnQuery ERROR: ' . a:msg 16 | echohl None 17 | endfunction 18 | 19 | function! s:throw(msg) abort 20 | let v:errmsg = 'MdnQuery: ' . a:msg 21 | throw v:errmsg 22 | endfunction 23 | 24 | function! s:busy() abort 25 | return s:hasJob() && s:async.active 26 | endfunction 27 | 28 | function! mdnquery#search(query, topics) abort 29 | if empty(a:query) 30 | call s:errorMsg('Missing search term') 31 | return 32 | endif 33 | if s:busy() 34 | call s:errorMsg('Cannot start another job before the current finished') 35 | return 36 | endif 37 | let s:pane.query = a:query 38 | let s:pane.topics = a:topics 39 | if s:history.HasList(a:query, a:topics) 40 | let s:pane.list = s:history.GetList(a:query, a:topics) 41 | if s:async.firstMatch && !empty(s:pane.list) 42 | call s:pane.OpenEntry(0) 43 | else 44 | call s:pane.ShowList() 45 | let s:async.firstMatch = 0 46 | endif 47 | return 48 | endif 49 | let s:pane.list = [] 50 | if s:hasJob() 51 | call s:asyncSearch(a:query, a:topics) 52 | else 53 | call s:syncSearch(a:query, a:topics) 54 | endif 55 | endfunction 56 | 57 | function! mdnquery#firstMatch(query, topics) abort 58 | let s:async.firstMatch = 1 59 | call mdnquery#search(a:query, a:topics) 60 | endfunction 61 | 62 | function! mdnquery#focus() abort 63 | call s:pane.SetFocus() 64 | endfunction 65 | 66 | function! mdnquery#toggle() abort 67 | if !s:pane.Exists() 68 | call s:pane.ShowList() 69 | return 70 | endif 71 | if s:pane.IsVisible() 72 | call s:pane.Hide() 73 | else 74 | call s:pane.Show() 75 | endif 76 | endfunction 77 | 78 | function! mdnquery#show() abort 79 | if s:pane.IsVisible() 80 | return 81 | endif 82 | call mdnquery#toggle() 83 | endfunction 84 | 85 | function! mdnquery#hide() abort 86 | if !s:pane.IsVisible() 87 | return 88 | endif 89 | call mdnquery#toggle() 90 | endfunction 91 | 92 | function! mdnquery#list() abort 93 | if s:pane.contentType == 'list' 94 | if !s:pane.IsVisible() 95 | call mdnquery#show() 96 | endif 97 | return 98 | endif 99 | call s:pane.ShowList() 100 | endfunction 101 | 102 | function! mdnquery#entry(num) abort 103 | if empty(s:pane.list) 104 | call s:errorMsg('No entries available') 105 | return 106 | endif 107 | if a:num < 1 || a:num > len(s:pane.list) 108 | call s:errorMsg('Entry number must be between 1 and ' . len(s:pane.list)) 109 | return 110 | endif 111 | let index = a:num - 1 112 | call s:pane.OpenEntry(index) 113 | endfunction 114 | 115 | function! mdnquery#entryUnderCursor() abort 116 | if !s:pane.IsFocused() 117 | call s:errorMsg('Must be inside a MdnQuery buffer') 118 | return 119 | endif 120 | if s:pane.contentType != 'list' 121 | return 122 | endif 123 | if s:busy() 124 | call s:errorMsg('Cannot start another job before the current finished') 125 | return 126 | endif 127 | let line = getline('.') 128 | let match = matchlist(line, '^\(\d\+\))') 129 | if empty(match) 130 | call s:errorMsg('Not a valid entry') 131 | return 132 | endif 133 | let index = match[1] - 1 134 | call s:pane.OpenEntry(index) 135 | endfunction 136 | 137 | function! mdnquery#statusline() abort 138 | if s:pane.contentType == 'list' && !empty(s:pane.query) 139 | return 'MdnQuery - search results for: ' . s:pane.Target() 140 | elseif s:pane.contentType == 'entry' 141 | return 'MdnQuery - documentation for: ' . s:pane.currentEntry 142 | else 143 | return 'MdnQuery' 144 | endif 145 | endfunction 146 | 147 | function! mdnquery#topics() abort 148 | " Vim has v:t_list, which does not exist in NeoVim 149 | let listType = 3 150 | if exists('b:mdnquery_topics') && type(b:mdnquery_topics) == listType 151 | \ && !empty(b:mdnquery_topics) 152 | return b:mdnquery_topics 153 | elseif type(g:mdnquery_topics) == listType && !empty(g:mdnquery_topics) 154 | return g:mdnquery_topics 155 | endif 156 | return ['js'] 157 | endfunction 158 | 159 | " History 160 | let s:history = { 161 | \ 'list': {}, 162 | \ 'entries': {} 163 | \ } 164 | 165 | function! s:history.HasEntry(url) abort 166 | return has_key(self.entries, a:url) 167 | endfunction 168 | 169 | function! s:history.HasList(query, topics) abort 170 | return !empty(self.GetList(a:query, a:topics)) 171 | endfunction 172 | 173 | function! s:history.GetEntry(url) abort 174 | return get(self.entries, a:url, {}) 175 | endfunction 176 | 177 | function! s:history.GetList(query, topics) abort 178 | let section = get(self.list, string(a:topics), {}) 179 | return get(section, a:query, []) 180 | endfunction 181 | 182 | function! s:history.SetEntry(entry) abort 183 | let s:history.entries[a:entry.url] = a:entry.content 184 | endfunction 185 | 186 | function! s:history.SetList(list, query, topics) abort 187 | let topics = string(a:topics) 188 | if !has_key(s:history.list, topics) 189 | let s:history.list[topics] = {} 190 | endif 191 | let s:history.list[topics][a:query] = a:list 192 | endfunction 193 | 194 | " Pane 195 | let s:pane = { 196 | \ 'bufname': 'mdnquery-buffer', 197 | \ 'list': [], 198 | \ 'query': '', 199 | \ 'topics': [], 200 | \ 'contentType': 'none', 201 | \ 'currentEntry': '' 202 | \ } 203 | 204 | function! s:pane.Create() abort 205 | if self.Exists() 206 | return 207 | endif 208 | let prevwin = winnr() 209 | execute 'silent ' . self.BufferOptions() . ' new ' . self.bufname 210 | setfiletype mdnquery 211 | setlocal syntax=markdown 212 | setlocal noswapfile 213 | setlocal buftype=nowrite 214 | setlocal bufhidden=hide 215 | setlocal nobuflisted 216 | setlocal nomodifiable 217 | setlocal nospell 218 | setlocal statusline=%!mdnquery#statusline() 219 | nnoremap :call mdnquery#entryUnderCursor() 220 | nnoremap o :call mdnquery#entryUnderCursor() 221 | nnoremap r :call mdnquery#list() 222 | if g:mdnquery_auto_focus 223 | if mode() == 'i' 224 | silent stopinsert 225 | endif 226 | else 227 | if prevwin != winnr() 228 | execute prevwin . 'wincmd w' 229 | endif 230 | endif 231 | endfunction 232 | 233 | function! s:pane.Destroy() abort 234 | let bufnr = bufnr(self.bufname) 235 | if bufnr != -1 236 | execute 'bwipeout ' . bufnr 237 | endif 238 | endfunction 239 | 240 | function! s:pane.Exists() abort 241 | return bufloaded(self.bufname) 242 | endfunction 243 | 244 | function! s:pane.IsVisible() abort 245 | if bufwinnr(self.bufname) == -1 246 | return 0 247 | else 248 | return 1 249 | endif 250 | endfunction 251 | 252 | function! s:pane.IsFocused() abort 253 | return bufwinnr(self.bufname) == winnr() 254 | endfunction 255 | 256 | function! s:pane.SetFocus() abort 257 | let winnr = bufwinnr(self.bufname) 258 | if !self.IsVisible() || winnr == winnr() 259 | return 260 | endif 261 | execute winnr . 'wincmd w' 262 | endfunction 263 | 264 | function! s:pane.Show() abort 265 | if self.IsVisible() 266 | return 267 | endif 268 | let prevwin = winnr() 269 | execute 'silent ' . self.BufferOptions() . ' split' 270 | execute 'silent buffer ' . self.bufname 271 | if g:mdnquery_auto_focus 272 | if mode() == 'i' 273 | silent stopinsert 274 | endif 275 | else 276 | if prevwin != winnr() 277 | execute prevwin . 'wincmd w' 278 | endif 279 | endif 280 | endfunction 281 | 282 | function! s:pane.Hide() abort 283 | if !self.IsVisible() 284 | return 285 | endif 286 | call self.SetFocus() 287 | quit 288 | endfunction 289 | 290 | function! s:pane.ShowList() abort 291 | let lines = map(copy(self.list), "v:val.id . ') ' . v:val.title") 292 | call insert(lines, self.Title()) 293 | call self.SetContent(lines) 294 | let self.contentType = 'list' 295 | silent doautocmd User MdnQueryContentChange 296 | endfunction 297 | 298 | function! s:pane.ShowEntry(id) abort 299 | let entry = get(self.list, a:id, {}) 300 | if !exists('entry.content') || empty(entry.content) 301 | return 302 | endif 303 | call self.SetContent(entry.content) 304 | let self.contentType = 'entry' 305 | let self.currentEntry = entry.title 306 | silent doautocmd User MdnQueryContentChange 307 | endfunction 308 | 309 | function! s:pane.SetContent(lines) abort 310 | let prevwin = winnr() 311 | if self.Exists() 312 | call self.Show() 313 | else 314 | call self.Create() 315 | endif 316 | call self.SetFocus() 317 | setlocal modifiable 318 | " Delete content into blackhole register 319 | silent %d_ 320 | call append(0, a:lines) 321 | " Delete empty line at the end 322 | silent $d_ 323 | call cursor(1, 1) 324 | setlocal nomodifiable 325 | if g:mdnquery_auto_focus 326 | if mode() == 'i' 327 | silent stopinsert 328 | endif 329 | else 330 | if prevwin != winnr() 331 | execute prevwin . 'wincmd w' 332 | endif 333 | endif 334 | endfunction 335 | 336 | function! s:pane.Title() abort 337 | if empty(self.query) 338 | return 'No search results' 339 | endif 340 | if empty(self.list) 341 | return 'No search results for ' . self.Target() 342 | endif 343 | return 'Search results for ' . self.Target() 344 | endfunction 345 | 346 | function! s:pane.Target() abort 347 | return self.query . ' (topics: ' . join(self.topics, ', ') . ')' 348 | endfunction 349 | 350 | function! s:pane.OpenEntry(index) abort 351 | let s:async.firstMatch = 0 352 | let entry = get(self.list, a:index, {}) 353 | if exists('entry.content') && !empty(entry.content) 354 | call self.ShowEntry(a:index) 355 | elseif s:history.HasEntry(entry.url) 356 | let entry.content = s:history.GetEntry(entry.url) 357 | call self.ShowEntry(a:index) 358 | else 359 | if s:hasJob() 360 | call s:asyncOpenEntry(a:index) 361 | else 362 | call s:syncOpenEntry(a:index) 363 | endif 364 | endif 365 | endfunction 366 | 367 | function! s:pane.BufferOptions() abort 368 | let size = exists('g:mdnquery_size') ? ' ' . g:mdnquery_size . ' ' : '' 369 | let vertical = g:mdnquery_vertical ? ' vertical ' : '' 370 | return 'botright' . vertical . size 371 | endfunction 372 | 373 | " Async jobs 374 | let s:async = { 375 | \ 'active': 0, 376 | \ 'firstMatch': 0, 377 | \ 'error': 0 378 | \ } 379 | 380 | function! s:jobStart(script, callbacks) abort 381 | let cmd = ['ruby', '-e', 'require "mdn_query"', '-e', a:script] 382 | if has('nvim') 383 | let jobId = jobstart(cmd, a:callbacks) 384 | if jobId > 0 385 | let s:async.active = 1 386 | endif 387 | else 388 | let job = job_start(cmd, a:callbacks) 389 | if job_status(job) != 'fail' 390 | let s:async.active = 1 391 | endif 392 | endif 393 | endfunction 394 | 395 | function! s:finishJobEntry(...) abort 396 | if s:async.error 397 | call s:pane.ShowList() 398 | else 399 | let entry = s:pane.list[s:async.currentIndex] 400 | call s:pane.ShowEntry(s:async.currentIndex) 401 | call s:history.SetEntry(entry) 402 | endif 403 | unlet s:async.currentIndex 404 | let s:async.active = 0 405 | let s:async.error = 0 406 | endfunction 407 | 408 | function! s:finishJobList(...) abort 409 | call s:history.SetList(s:pane.list, s:pane.query, s:pane.topics) 410 | if s:async.firstMatch && !empty(s:pane.list) 411 | call s:pane.OpenEntry(0) 412 | else 413 | call s:pane.ShowList() 414 | let s:async.active = 0 415 | let s:async.firstMatch = 0 416 | let s:async.error = 0 417 | endif 418 | endfunction 419 | 420 | function! s:nvimHandleSearch(id, data, event) abort 421 | " Remove last empty line 422 | call remove(a:data, -1) 423 | for entry in a:data 424 | let escaped = s:escapeDict(entry) 425 | call add(s:pane.list, eval(escaped)) 426 | endfor 427 | endfunction 428 | 429 | function! s:nvimHandleEntry(id, data, event) abort 430 | call extend(s:pane.list[s:async.currentIndex].content, a:data) 431 | endfunction 432 | 433 | function! s:nvimHandleError(id, data, event) abort 434 | call s:msg(join(a:data)) 435 | let s:async.error = 1 436 | endfunction 437 | 438 | function! s:vimHandleSearch(channel, msg) abort 439 | let escaped = s:escapeDict(a:msg) 440 | call add(s:pane.list, eval(escaped)) 441 | endfunction 442 | 443 | function! s:vimHandleEntry(channel, msg) abort 444 | call add(s:pane.list[s:async.currentIndex].content, a:msg) 445 | endfunction 446 | 447 | function! s:vimHandleError(channel, msg) abort 448 | call s:msg(a:msg) 449 | let s:async.error = 1 450 | endfunction 451 | 452 | function! s:syncSearch(query, topics) abort 453 | ruby << EOF 454 | begin 455 | query = VIM.evaluate('a:query') 456 | topics = VIM.evaluate('a:topics') 457 | list = MdnQuery.list(query, topics: topics) 458 | list.each do |e| 459 | id = VIM.evaluate('len(s:pane.list)') + 1 460 | item = "{ 'id': #{id}, 'title': '#{e.title}', 'url': '#{e.url}' }" 461 | escaped = item.gsub(/(\w)'(\w)/, '\1\'\'\2') 462 | VIM.evaluate("add(s:pane.list, #{escaped})") 463 | end 464 | rescue MdnQuery::NoEntryFound 465 | VIM.evaluate("s:msg('No results for #{query}')") 466 | rescue MdnQuery::HttpRequestFailed 467 | VIM.evaluate("s:msg('Network error')") 468 | end 469 | EOF 470 | call s:history.SetList(s:pane.list, s:pane.query, s:pane.topics) 471 | if s:async.firstMatch && !empty(s:pane.list) 472 | call s:pane.OpenEntry(0) 473 | else 474 | call s:pane.ShowList() 475 | let s:async.firstMatch = 0 476 | endif 477 | endfunction 478 | 479 | function! s:asyncSearch(query, topics) abort 480 | let index = len(s:pane.list) 481 | let topics = string(a:topics) 482 | let script = "begin;" 483 | \ . " list = MdnQuery.list('" . a:query . "', topics: " . topics . ");" 484 | \ . " i = " . index . ";" 485 | \ . " entries = list.items.map do |e|;" 486 | \ . " i += 1;" 487 | \ . " \"{ 'id': #{i}, 'title': '#{e.title}', 'url': '#{e.url}' }\"" 488 | \ . " end;" 489 | \ . " puts entries;" 490 | \ . "rescue MdnQuery::NoEntryFound;" 491 | \ . " STDERR.puts 'No results for " . a:query . "';" 492 | \ . "rescue MdnQuery::HttpRequestFailed;" 493 | \ . " STDERR.puts 'Network error';" 494 | \ . "end" 495 | if has('nvim') 496 | let callbacks = { 497 | \ 'on_stdout': function('s:nvimHandleSearch'), 498 | \ 'on_stderr': function('s:nvimHandleError'), 499 | \ 'on_exit': function('s:finishJobList') 500 | \ } 501 | else 502 | let callbacks = { 503 | \ 'out_cb': function('s:vimHandleSearch'), 504 | \ 'err_cb': function('s:vimHandleError'), 505 | \ 'close_cb': function('s:finishJobList') 506 | \ } 507 | endif 508 | if g:mdnquery_show_on_invoke 509 | call mdnquery#show() 510 | endif 511 | if s:pane.IsVisible() 512 | call s:pane.SetContent('>> Searching for ' . s:pane.Target() . '...') 513 | endif 514 | 515 | return s:jobStart(script, callbacks) 516 | endfunction 517 | function! s:syncOpenEntry(index) abort 518 | let entry = get(s:pane.list, a:index, {}) 519 | if !exists('entry.url') 520 | return 521 | endif 522 | if !exists('entry.content') 523 | try 524 | let entry.content = s:DocumentFromUrl(entry.url) 525 | catch /MdnQuery:/ 526 | echomsg v:errmsg 527 | return 528 | endtry 529 | endif 530 | call s:pane.ShowEntry(a:index) 531 | call s:history.SetEntry(entry) 532 | endfunction 533 | 534 | function! s:asyncOpenEntry(index) abort 535 | let entry = get(s:pane.list, a:index, {}) 536 | if exists('entry.content') && !empty(entry.content) 537 | call s:pane.ShowEntry(a:index) 538 | return 539 | endif 540 | if !exists('entry.url') 541 | return 542 | endif 543 | let s:async.currentIndex = a:index 544 | let entry.content = [] 545 | if has('nvim') 546 | let callbacks = { 547 | \ 'on_stdout': function('s:nvimHandleEntry'), 548 | \ 'on_stderr': function('s:nvimHandleError'), 549 | \ 'on_exit': function('s:finishJobEntry') 550 | \ } 551 | else 552 | let callbacks = { 553 | \ 'out_cb': function('s:vimHandleEntry'), 554 | \ 'err_cb': function('s:vimHandleError'), 555 | \ 'close_cb': function('s:finishJobEntry') 556 | \ } 557 | endif 558 | let script = "begin;" 559 | \ . " document = MdnQuery::Document.from_url('" . entry.url . "');" 560 | \ . " puts document;" 561 | \ . "rescue MdnQuery::HttpRequestFailed;" 562 | \ . " STDERR.puts 'Network error';" 563 | \ . "end" 564 | if g:mdnquery_show_on_invoke 565 | call mdnquery#show() 566 | endif 567 | if s:pane.IsVisible() 568 | call s:pane.SetContent('>> Fetching ' . entry.title . '...') 569 | endif 570 | 571 | return s:jobStart(script, callbacks) 572 | endfunction 573 | 574 | function! s:DocumentFromUrl(url) abort 575 | let lines = [] 576 | ruby << EOF 577 | begin 578 | url = VIM.evaluate('a:url') 579 | document = MdnQuery::Document.from_url(url) 580 | document.to_md.each_line do |line| 581 | escaped = line.gsub('"', '\"').chomp 582 | VIM.evaluate("add(lines, \"#{escaped}\")") 583 | end 584 | rescue MdnQuery::HttpRequestFailed 585 | VIM.evaluate("s:throw('Network error')") 586 | end 587 | EOF 588 | return lines 589 | endfunction 590 | 591 | function! s:escapeDict(dict) abort 592 | " Escape single quotes inside dictionary values 593 | return substitute(a:dict, '\([^ :,]\)''\([^ :,]\)', '\1''''\2', 'g') 594 | endfunction 595 | -------------------------------------------------------------------------------- /doc/mdnquery.txt: -------------------------------------------------------------------------------- 1 | *mdnquery.txt* Query the Mozilla Developer Network documentation 2 | 3 | MdnQuery 4 | 5 | ============================================================================== 6 | CONTENTS *mdnquery-contents* 7 | 8 | 1. Introduction ................................. |mdnquery-introduction| 9 | 2. Usage ........................................ |mdnquery-usage| 10 | 3. Commands ..................................... |mdnquery-commands| 11 | 4. Functions .................................... |mdnquery-functions| 12 | 5. Mappings ..................................... |mdnquery-mappings| 13 | 5.1. Buffer mappings .......................... |mdnquery-mappings-buffer| 14 | 5.2. Normal mode mappings ..................... |mdnquery-mappings-normal| 15 | 5.3. Visual mode mappings ..................... |mdnquery-mappings-visual| 16 | 6. Settings ..................................... |mdnquery-settings| 17 | 7. Autocommands ................................. |mdnquery-autocmds| 18 | 8. Keywordprg (K command) ....................... |mdnquery-keywordprg| 19 | 20 | ============================================================================== 21 | 1. Introduction *mdnquery-introduction* 22 | 23 | Query the Mozilla Developer Network documentation without leaving Vim. 24 | A Markdown version of the documentation is loaded into a buffer. The network 25 | requests are done asynchronously if the |job-control| feature is available 26 | (both in NeoVim and Vim), otherwise it falls back to using |ruby|. The search 27 | results and documentation entries are cached to avoid unnecessary requests. 28 | 29 | ============================================================================== 30 | 2. Usage *mdnquery-usage* 31 | 32 | The basic usage is straightforward. To search for a query call the |:MdnQuery| 33 | command with said query and it will open a |mdnquery-buffer| with a list of 34 | search results. If |job-control| is available, it might seem like nothing is 35 | happening at first, but when the search finishes, the buffer will be shown. 36 | Example: > 37 | MdnQuery array remove 38 | < 39 | Inside the buffer you can open the entry under the cursor by pressing 40 | and press `r` to return to the list. 41 | Often the search query is specific enough such that the first result is the 42 | desired documentation entry, and it certainly gets annoying to manually open 43 | that every time. For this case the |:MdnQueryFirstMatch| command exists, which 44 | simply opens the first entry automatically. 45 | 46 | By default the searches are limited to JavaScript but can be changed with the 47 | setting |g:mdnquery_topics|. Additionally there are several settings to change 48 | how and when the buffer should be displayed (see |mdnquery-settings|). 49 | 50 | *mdnquery-topics* 51 | The commands only search in the topics specified in |g:mdnquery_topics|. 52 | Example: > 53 | let g:mdnquery_topics = ['js', 'css', 'html'] 54 | " Default 55 | let g:mdnquery_topics = ['js'] 56 | < 57 | Having a global list of topics for all searches might give results with 58 | entries from an irrelevant topic. Instead of having to change the global 59 | option, the setting |b:mdnquery_topics|, which is local to the current buffer, 60 | can be set. This can easily be combined with an |autocmd| to set the correct 61 | topics for a specific file type. 62 | Example: > 63 | autocmd FileType html let b:mdnquery_topics = ['css', 'html'] 64 | < 65 | What if even that setting is too much of a hassle? Maybe you want to execute 66 | just one search for a specific topic without changing the current settings. 67 | This can be done by calling the function |mdnquery#search| or 68 | |mdnquery#firstMatch|. 69 | Example: > 70 | call mdnquery#search('link', ['css', 'html']) 71 | call mdnquery#firstMatch('flex align', ['css']) 72 | < 73 | *mdnquery-buffer* 74 | The |mdnquery-buffer| is a unique buffer that displays all relevant 75 | informations of this plugin. It is opened with the |:botright| command and 76 | therefore appears at full width on the bottom of the screen, or when 77 | |g:mdnquery_vertical| is set, it appears at full height on the very right of 78 | the screen. The size of the buffer can be changed with the |g:mdnquery_size| 79 | setting. By default the buffer appears after a search is completed and it is 80 | not automatically focused. You can change this behaviour by changing the 81 | |g:mdnquery_show_on_invoke| and |g:mdnquery_auto_focus| settings. The buffer 82 | provides some mappings to easily navigate through the search results, see 83 | |mdnquery-mappings-buffer|. 84 | 85 | *mdnquery-filetype* 86 | The file type of the |mdnquery-buffer| is set to "mdnquery" and uses the 87 | syntax of markdown. If you lazily load any plugins for markdown, you also need 88 | to add "mdnquery" to activate them. Having a custom file type allows you to 89 | easily modify the settings of the buffer. For instance you can add local 90 | mappings or set a specific option: > 91 | " switches back to the previously selected window 92 | autocmd FileType mdnquery nnoremap p 93 | autocmd FileType mdnquery nnoremap q :MdnQueryToggle 94 | 95 | " Disable visual line wrapping 96 | autocmd FileType mdnquery setlocal nowrap 97 | < 98 | 99 | ============================================================================== 100 | 3. Commands *mdnquery-commands* 101 | 102 | *:MdnQuery* 103 | :MdnQuery {query} Searches for {query} and shows the list of results in 104 | the |mdnquery-buffer|. The search is limited to the 105 | configured topics (see |g:mdnquery_topics|). 106 | 107 | *:MdnQueryFirstMatch* 108 | :MdnQueryFirstMatch {query} 109 | Searches for {query} and loads the first result of the 110 | list into the |mdnquery-buffer|. The search is limited 111 | to the configured topics (see |g:mdnquery_topics|). 112 | 113 | *:MdnQueryList* 114 | :MdnQueryList Shows the list of the current search results in the 115 | |mdnquery-buffer|. 116 | 117 | *:MdnQueryToggle* 118 | :MdnQueryToggle Toggles the visibility of the |mdnquery-buffer|. Use 119 | |mdnquery#show| and |mdnquery#hide| if you prefer to 120 | show or hide it unconditionally. 121 | 122 | ============================================================================== 123 | 4. Functions *mdnquery-functions* 124 | 125 | *mdnquery#entry* 126 | mdnquery#entry({number}) 127 | Opens the entry with the {number} of the current list 128 | and loads it into the |mdnquery-buffer|. As long as 129 | there is a list and the entry {number} exists, it is 130 | loaded, regardless of whether the list is currently 131 | shown. 132 | 133 | *mdnquery#entryUnderCursor* 134 | mdnquery#entryUnderCursor() 135 | Opens the entry under the cursor. Only available in 136 | |mdnquery-buffer||. 137 | 138 | *mdnquery#firstMatch* 139 | mdnquery#firstMatch({string}, {list}) 140 | Searches for {string} in the topics given by {list} 141 | and loads the first result of the list into the 142 | |mdnquery-buffer|. 143 | *mdnquery#focus* 144 | mdnquery#focus() Sets focus to the |mdnquery-buffer|. 145 | 146 | *mdnquery#hide* 147 | mdnquery#hide() Hides the |mdnquery-buffer|. 148 | 149 | *mdnquery#list* 150 | mdnquery#list() Shows the list of the current search results in the 151 | |mdnquery-buffer|. 152 | 153 | *mdnquery#search* 154 | mdnquery#search({string}, {list}) 155 | Searches for {string} in the topics given by {list} 156 | and shows the list of results in the |mdnquery-buffer|. 157 | 158 | 159 | *mdnquery#show* 160 | mdnquery#show() Shows the |mdnquery-buffer|. 161 | 162 | *mdnquery#statusline* 163 | mdnquery#statusline() Returns the message that is displayed in the 164 | status line of the |mdnquery-buffer|. It describes 165 | what is currently being displayed. 166 | 167 | *mdnquery#toggle* 168 | mdnquery#toggle() Toggles the visibility of the |mdnquery-buffer|. 169 | 170 | *mdnquery#topics* 171 | mdnquery#topics() Returns the topics that are currently being used for 172 | any search command (see |g:mdnquery_topics|). 173 | 174 | ============================================================================== 175 | 5. Mappings *mdnquery-mappings* 176 | 177 | ------------------------------------------------------------------------------ 178 | 5.1. Buffer mappings *mdnquery-mappings-buffer* 179 | 180 | The following mappings are available in the |mdnquery-buffer|. If you would 181 | like to have custom mappings with the same effect, use the respective 182 | functions or set mappings for the |mdnquery-filetype|. 183 | 184 | *mdnquery-buffer-r* 185 | r Returns to the list of the current search results. 186 | The same effect can be achieved by calling 187 | |mdnquery#list|. 188 | 189 | *mdnquery-buffer-enter* 190 | *mdnquery-buffer-o* 191 | 192 | o Opens the entry under the cursor. It simply calls 193 | |mdnquery#entryUnderCursor|. This function is only 194 | available inside the |mdnquery-buffer|. To achieve 195 | a similar functionality from outside the buffer, use 196 | |mdnquery#entry|. 197 | 198 | ------------------------------------------------------------------------------ 199 | 5.2. Normal mode mappings *mdnquery-mappings-normal* 200 | 201 | *MdnqueryEntry* 202 | [count]MdnqueryEntry 203 | Opens the entry with the number [count] of the current 204 | list. If no [count] is given, it opens the first one. 205 | Example: > 206 | :nmap e MdnqueryEntry 207 | 3e " Opens the 3rd entry 208 | e " Opens the 1st entry 209 | < 210 | *MdnqueryWordsearch* 211 | MdnqueryWordsearch 212 | Searches for the word under the cursor. 213 | 214 | *MdnqueryWordfirstmatch* 215 | MdnqueryWordfirstmatch 216 | Searches for the word under the cursor and opens the 217 | first entry. 218 | 219 | ------------------------------------------------------------------------------ 220 | 5.3. Visual mode mappings *mdnquery-mappings-visual* 221 | 222 | *MdnqueryVisualsearch* 223 | MdnqueryVisualsearch 224 | Searches for the selected text. Selections over 225 | multiple lines are joined by a space and excessive 226 | whitespace is removed from the entire selection. 227 | 228 | *MdnqueryVisualfirstmatch* 229 | MdnqueryVisualfirstmatch 230 | Searches for the selected text and opens the first 231 | entry. Selections over multiple lines are joined by 232 | a space and excessive whitespace is removed from the 233 | entire selection. 234 | 235 | ============================================================================== 236 | 6. Settings *mdnquery-settings* 237 | 238 | *g:mdnquery_auto_focus* 239 | g:mdnquery_auto_focus {bool} (Default: 0) 240 | When set to 1, all actions invoked automatically set 241 | the focus to the |mdnquery-buffer|. If you prefer to 242 | focus it only after a command finishes, you can use 243 | the autocommand |MdnQueryContentChange| instead. 244 | 245 | *g:mdnquery_javascript_man* 246 | g:mdnquery_javascript_man {string} (Default: "firstMatch") 247 | Defines the behaviour of the 'keywordprg' used by the 248 | |K| command for JavaScript files. When {string} is 249 | "search" it executes |:MdnQuery| and "firstMatch" 250 | executes |:MdnQueryFirstMatch|. If {string} is 251 | anything else, the 'keywordprg' is not set. 252 | 253 | *g:mdnquery_show_on_invoke* 254 | g:mdnquery_show_on_invoke {bool} (Default: 0) 255 | When set to 1, automatically shows the 256 | |mdnquery-buffer| whenever an action is invoked. This 257 | only makes a difference when the buffer is hidden. 258 | 259 | *g:mdnquery_size* 260 | g:mdnquery_size {number} 261 | Sets the size of the |mdnquery-buffer|. When 262 | |g:mdnquery_vertical| is 1, it corresponds to its 263 | width, otherwise to its height. 264 | 265 | *g:mdnquery_topics* 266 | g:mdnquery_topics {list} (Default: ["js"]) 267 | Topics to search in. All commands use these topics 268 | unless |b:mdnquery_topics| is present or it is not 269 | a valid list, in which case it uses the default. Any 270 | topic that is listed on the Mozilla Developer Network 271 | search page can be used (the term specified in the URL 272 | is used). 273 | 274 | *b:mdnquery_topics* 275 | b:mdnquery_topics {list} 276 | Same as |g:mdnquery_topics| but local to the current 277 | buffer. It has a higher priority than the global 278 | option. 279 | 280 | *g:mdnquery_vertical* 281 | g:mdnquery_vertical {bool} (Default: 0) 282 | When set to 1, |mdnquery-buffer| is a vertical split, 283 | otherwise it is a horizontal split. 284 | 285 | ============================================================================== 286 | 7. Autocommands *mdnquery-autocmds* 287 | 288 | *MdnQueryContentChange* 289 | The |MdnQueryContentChange| autocommand is triggered when the content of the 290 | |mdnquery-buffer| changes. This does not include any messages that signalise 291 | a running job. 292 | A simple example is to focus the buffer whenever it changes: > 293 | autocmd User MdnQueryContentChange call mdnquery#focus() 294 | < 295 | 296 | ============================================================================== 297 | 8. Keywordprg (K command) *mdnquery-keywordprg* 298 | 299 | The 'keywordprg' is used by the |K| command. By default it uses the "man" 300 | command on Unix and `:help` otherwise. The default behaviour is not very 301 | useful for many file types. This plugin automatically changes the 'keywordprg' 302 | for JavaScript files, which can be configured with the 303 | |g:mdnquery_javascript_man|. 304 | 305 | As Mozilla Developer Network also provides documentation for many other web 306 | technologies, it might be desired to set the 'keywordprg' for other file 307 | types. This can easily be done with an |autocmd|. 308 | Example: > 309 | autocmd FileType html setlocal keywordprg=:MdnQueryFirstMatch 310 | < 311 | In this case it does not make much sense to use the same topics for the 312 | search. The global setting |g:mdnquery_topics| could be set such that it works 313 | for all the needed file types, but that is generally not a practical solution. 314 | Instead the buffer setting |b:mdnquery_topics| can be set, which conveniently 315 | can also be done with an |autocmd|. 316 | Example: > 317 | autocmd FileType html let b:mdnquery_topics = ['css', 'html'] 318 | autocmd FileType html setlocal keywordprg=:MdnQuery 319 | < 320 | *mdnquery-keywordprg-alternative* 321 | There exist alternatives with the same functionality for situation when 322 | 'keywordprg' is not appropriate or desirable, see |MdnqueryWordsearch| and 323 | |MdnqueryVisualsearch| and their first match variants. 324 | 325 | vim:tw=78:ft=help:et:ts=2:sw=2:sts=2:norl 326 | -------------------------------------------------------------------------------- /plugin/mdnquery.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_mdnquery') || &compatible 2 | finish 3 | endif 4 | let g:loaded_mdnquery = 1 5 | 6 | if !exists('g:mdnquery_vertical') 7 | let g:mdnquery_vertical = 0 8 | endif 9 | 10 | if !exists('g:mdnquery_auto_focus') 11 | let g:mdnquery_auto_focus = 0 12 | endif 13 | 14 | if !exists('g:mdnquery_topics') 15 | let g:mdnquery_topics = ['js'] 16 | endif 17 | 18 | if !exists('g:mdnquery_show_on_invoke') 19 | let g:mdnquery_show_on_invoke = 0 20 | endif 21 | 22 | if !exists('g:mdnquery_javascript_man') 23 | let g:mdnquery_javascript_man = 'firstMatch' 24 | endif 25 | 26 | augroup mdnquery_javascript 27 | autocmd! 28 | autocmd FileType javascript call s:setKeywordprg() 29 | augroup END 30 | 31 | command! -nargs=* -bar MdnQuery 32 | \ call mdnquery#search(, mdnquery#topics()) 33 | command! -nargs=* -bar MdnQueryFirstMatch 34 | \ call mdnquery#firstMatch(, mdnquery#topics()) 35 | command! -nargs=0 -bar MdnQueryList call mdnquery#list() 36 | command! -nargs=0 -bar MdnQueryToggle call mdnquery#toggle() 37 | 38 | nnoremap MdnqueryEntry :call mdnquery#entry(v:count1) 39 | nnoremap MdnqueryWordsearch 40 | \ :call mdnquery#search(expand(''), mdnquery#topics()) 41 | nnoremap MdnqueryWordfirstmatch 42 | \ :call mdnquery#firstMatch(expand(''), mdnquery#topics()) 43 | xnoremap MdnqueryVisualsearch 44 | \ :call mdnquery#search(selected(), mdnquery#topics()) 45 | xnoremap MdnqueryVisualfirstmatch 46 | \ :call mdnquery#firstMatch(selected(), mdnquery#topics()) 47 | 48 | function! s:selected() abort 49 | let old_z = @z 50 | silent normal! gv"zy 51 | let query = s:removeWhitespace(@z) 52 | let @z = old_z 53 | return query 54 | endfunction 55 | 56 | function! s:removeWhitespace(str) abort 57 | let str = substitute(a:str, '\n\|\r\|\s\+', ' ', 'g') 58 | let trimmed = substitute(str, '^\s\+\|\s\+$', '', 'g') 59 | return trimmed 60 | endfunction 61 | 62 | function! s:setKeywordprg() abort 63 | if g:mdnquery_javascript_man == 'firstMatch' 64 | setlocal keywordprg=:MdnQueryFirstMatch 65 | elseif g:mdnquery_javascript_man == 'search' 66 | setlocal keywordprg=:MdnQuery 67 | endif 68 | endfunction 69 | -------------------------------------------------------------------------------- /screenshots/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jungomi/vim-mdnquery/967dbde537bb443c08202dd8fdf3802408ee8099/screenshots/demo.gif --------------------------------------------------------------------------------