├── demo.gif ├── syntax └── markdrawer.vim ├── profile.runner ├── test ├── vimrc ├── run-tests.sh ├── simple.md ├── ui.vader ├── header.vader ├── indexing.vader └── levels.vader ├── ftplugin └── markdown.vim ├── LICENSE ├── README.md └── autoload ├── levels.vim └── ui.vim /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Scuilion/markdown-drawer/HEAD/demo.gif -------------------------------------------------------------------------------- /syntax/markdrawer.vim: -------------------------------------------------------------------------------- 1 | if exists('b:current_syntax') 2 | finish 3 | endif 4 | 5 | let b:current_syntax = 'markdrawer' 6 | 7 | execute 'highlight ToDelete ctermfg=' . g:markdrawer_to_delete_color 8 | -------------------------------------------------------------------------------- /profile.runner: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | vim --cmd 'profile start profile.result' \ 4 | --cmd 'profile! file *ftplugin/markdown.vim' \ 5 | -c 'profdel file *ftplugin/markdown.vim' \ 6 | test/simple.md 7 | -------------------------------------------------------------------------------- /test/vimrc: -------------------------------------------------------------------------------- 1 | filetype off 2 | set nomodeline 3 | set runtimepath+=~/.vim/bundle/vader.vim 4 | set runtimepath+=~/.vim/bundle/markdown-drawer 5 | nnoremap md :MarkDrawer 6 | filetype plugin indent on 7 | syntax enable 8 | -------------------------------------------------------------------------------- /test/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Use privileged mode, to e.g. ignore $CDPATH. 4 | set -p 5 | GLOB=${1:-'*'} 6 | 7 | cd "$( dirname "${BASH_SOURCE[0]}" )" || exit 8 | 9 | : "${VADER_TEST_VIM:=vim}" 10 | eval "$VADER_TEST_VIM -Nu vimrc -c 'Vader! $GLOB'" 11 | -------------------------------------------------------------------------------- /test/simple.md: -------------------------------------------------------------------------------- 1 | #Main Title 2 | Main title text 3 | ### First Sub 4 | First paragraph 5 | ```viml 6 | " Used for fenced code test 7 | if !ReuseWindow() 8 | call CreateNewWindow() 9 | else 10 | ``` 11 | Next paragraph 12 | ### Second Sub Empty 13 | 14 | ## Moving 15 | moving row 1 # should not see this 16 | moving row 2 17 | ## First of Second 18 | Lorem Ipsum with 19 | multiple lines 20 | lines 21 | # Second Main 22 | A List 23 | - one 24 | - two 25 | `#` funky stuff 26 | -------------------------------------------------------------------------------- /ftplugin/markdown.vim: -------------------------------------------------------------------------------- 1 | " Maintainer: Kevin O'Neal oneal.kevin@gmail.com 2 | " Version: 0.4 3 | 4 | if !exists('g:markdrawer_prefix') 5 | let g:markdrawer_prefix = ' ' 6 | endif 7 | 8 | if !exists('g:markdrawer_goto') 9 | let g:markdrawer_goto = 'o' 10 | endif 11 | 12 | if !exists('g:markdrawerDelete') 13 | let g:markdrawerDelete = 'D' 14 | endif 15 | 16 | if !exists('g:markdrawer_width') 17 | let g:markdrawer_width = '25' 18 | endif 19 | 20 | if !exists('g:markdrawer_paste_below') 21 | let g:markdrawer_paste_below = 'p' 22 | endif 23 | 24 | if !exists('g:markdrawer_increase') 25 | let g:markdrawer_increase = '+' 26 | endif 27 | 28 | if !exists('g:markdrawer_decrease') 29 | let g:markdrawer_decrease = '-' 30 | endif 31 | 32 | if !exists('g:markdrawer_to_delete_color') 33 | let g:markdrawer_to_delete_color = 'Red' 34 | endif 35 | 36 | command! MarkDrawer :call ui#OpenMarkdownDrawer() 37 | command! -nargs=1 MarkDrawerLevelSet :call ui#MarkDrawerLevelSet() 38 | -------------------------------------------------------------------------------- /test/ui.vader: -------------------------------------------------------------------------------- 1 | "TODO: add test for empty vader buffer 2 | 3 | Given markdown (simple file): 4 | # first 5 | ## second 6 | 7 | Execute: 8 | unlet g:markdrawer_toc 9 | 10 | Do (open the drawer): 11 | \md 12 | 13 | Expect (simple tree): 14 | first 15 | second 16 | 17 | Given markdown (simple file): 18 | # first 19 | ## second 20 | # third 21 | ## fourth 22 | ### fifth 23 | 24 | Execute: 25 | let g:markdrawer_toc = 'full_index' 26 | 27 | Do (open the drawer): 28 | \md 29 | 30 | Expect (outline should be full indexed): 31 | 1. first 32 | 1. second 33 | 2. third 34 | 2. fourth 35 | 1. fifth 36 | 37 | Given markdown (simple file): 38 | # first 39 | ## second 40 | ## thrid 41 | # forth 42 | ## fifth 43 | ### sixth 44 | ## seventh 45 | 46 | Execute: 47 | let g:markdrawer_toc = 'index' 48 | 49 | Do (open the drawer): 50 | \md 51 | 52 | Expect (outline should be full indexed): 53 | 1. first 54 | 1. second 55 | 2. thrid 56 | 2. forth 57 | 1. fifth 58 | 1. sixth 59 | 2. seventh 60 | -------------------------------------------------------------------------------- /test/header.vader: -------------------------------------------------------------------------------- 1 | Before: 2 | let pattern = '\v^\s*(#+)\s*(.*)' 3 | 4 | Execute(simple header) : 5 | let actual = matchlist('# first', pattern) 6 | Assert len(actual) > 0, 'no matches found' 7 | AssertEqual '#', actual[1] 8 | AssertEqual 'first', actual[2] 9 | 10 | Execute(suport for space in front ) : 11 | let actual = matchlist(' ## second', pattern) 12 | Assert len(actual) > 0, 'no matches found' 13 | AssertEqual '##', actual[1] 14 | AssertEqual 'second', actual[2] 15 | 16 | Execute(crammed together) : 17 | let actual = matchlist('###third', pattern) 18 | Assert len(actual) > 0, 'no matches found' 19 | AssertEqual '###', actual[1] 20 | AssertEqual 'third', actual[2] 21 | 22 | Execute(no match found) : 23 | let actual = matchlist(' blah', pattern) 24 | Assert len(actual) == 0, 'should be empty' 25 | 26 | Execute(hash not in front) : 27 | let actual = matchlist('scary #', pattern) 28 | Assert len(actual) == 0, 'should be empty' 29 | 30 | Execute(escaped hash) : 31 | let actual = matchlist('`#`', pattern) 32 | Assert len(actual) == 0, 'should be empty' 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kevin O'Neal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/indexing.vader: -------------------------------------------------------------------------------- 1 | Given markdown (a simple markdown file): 2 | # first 3 | ## second 4 | # third 5 | ## fourth 6 | ### fifth 7 | 8 | Execute (do a full index): 9 | let g:markdrawer_toc = 'full_index' 10 | let actual = levels#MarkdownLevel() 11 | Assert len(actual) == 5 12 | Assert actual[0].index == 1 13 | Assert actual[1].index == 1 14 | Assert actual[2].index == 2 15 | Assert actual[3].index == 2 16 | Assert actual[4].index == 1 17 | "1. first 18 | " 1. second 19 | "2. third 20 | " 2. forth 21 | " 1. fifth 22 | 23 | Given markdown (a simple markdown file): 24 | # first 25 | ## second 26 | ## thrid 27 | # forth 28 | ## fifth 29 | ### sixth 30 | ## seventh 31 | 32 | Execute (do a relative index): 33 | let g:markdrawer_toc = 'index' 34 | let actual = levels#MarkdownLevel() 35 | Assert len(actual) == 7 36 | Assert actual[0].index == 1 37 | Assert actual[1].index == 1 38 | Assert actual[2].index == 2 39 | Assert actual[3].index == 2 40 | Assert actual[4].index == 1 41 | Assert actual[5].index == 1 42 | Assert actual[6].index == 2 43 | "1. first 44 | " 1. second 45 | " 2. thrid 46 | "2. forth 47 | " 1. fifth 48 | " 1. sixth 49 | " 2. seventh 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdown-drawer 2 | Simplify navigation in large markdown files. 3 | 4 | ### Demo 5 | 6 | ![Markdrawer Demo](https://github.com/scuilion/markdown-drawer/raw/master/demo.gif) 7 | 8 | ### Keys Mappings 9 | * `o` navigate to header in file. 10 | * `D` mark section for cut (no effect till paste). 11 | * `p` paste previously marked section below current. 12 | * `+` increase header size (removes `#`). 13 | * `-` decrease header size (adds `#`). 14 | 15 | ### Basic Options 16 | ```vim 17 | let g:markdrawer_prefix = " " 18 | let g:markdrawer_goto = "o" 19 | let g:markdrawerDelete = "D" 20 | let g:markdrawer_width = "25" 21 | let g:markdrawer_paste_below = "p" 22 | let g:markdrawer_increase = "+" 23 | let g:markdrawer_decrease = "-" 24 | let g:markdrawer_to_delete_color = "Red" 25 | ``` 26 | 27 | ### Effect Drawer Appearance 28 | ```vim 29 | let g:markdrawer_drawer_max_levels = 4 " max levels to display 30 | let g:markdrawer_toc = 'index' " displays as a TOC 31 | let g:markdrawer_toc = 'full_index' " displays as a TOC, no index reset 32 | ``` 33 | 34 | ### Mapping Recommendations 35 | Map key to open drawer: 36 | ``` 37 | nnoremap md :MarkDrawer 38 | ``` 39 | 40 | ### Commands 41 | 42 | * `:MarkDrawer` activate plugin 43 | * `:MarkDrawerLevelSet 3` set the max level to display 44 | 45 | ### License 46 | MIT 47 | -------------------------------------------------------------------------------- /test/levels.vader: -------------------------------------------------------------------------------- 1 | Given markdown (a simple markdown file): 2 | # Main Title 3 | something 4 | ### First Sub 5 | else 6 | multi 7 | #### Second Second 8 | 9 | # Second Main 10 | A list 11 | - one 12 | - two 13 | ### 14 | ## First of Second 15 | Some lorem Ipsum with 16 | multiple line 17 | 18 | Execute (MarkdownLevel): 19 | let actual = levels#MarkdownLevel() 20 | Assert len(actual) == 6 21 | 22 | Then (only one row should be marked active): 23 | let i = 0 24 | let activeCount = 0 25 | while i<= len(actual) - 1 26 | if actual[i].active == 1 27 | let activeCount = activeCount + 1 28 | endif 29 | let i = i+1 30 | endwhile 31 | Assert activeCount == 1 32 | 33 | Given markdown (a markdown file with fencing): 34 | # First Title 35 | ``` 36 | #!/usr/bin/env bash 37 | #A comment 38 | cat foo.txt 39 | ``` 40 | # Second Title 41 | 42 | Execute (Hash symbol in fenced code should be ignored): 43 | let actual = levels#MarkdownLevel() 44 | Assert len(actual) == 2 45 | 46 | Given markdown (a mardown file with missing title text): 47 | ### 48 | 49 | Execute (default header will be substituted): 50 | let actual = levels#MarkdownLevel() 51 | Assert actual[0].header == '' 52 | 53 | Given markdown (an empty file): 54 | Execute (empty file does not blow up): 55 | let actual = levels#MarkdownLevel() 56 | Assert len(actual) == 0 57 | 58 | Given markdown (no headers): 59 | some text 60 | 61 | Execute (no headers does not blow up): 62 | let actual = levels#MarkdownLevel() 63 | Assert len(actual) == 0 64 | 65 | Given markdown (two levels): 66 | # first 67 | ## second 68 | 69 | Execute (set header level to one): 70 | let g:markdrawer_drawer_max_levels=1 71 | let actual = levels#MarkdownLevel() 72 | unlet g:markdrawer_drawer_max_levels 73 | Assert len(actual) == 1 74 | Assert actual[0].header == 'first' 75 | 76 | Given markdown (simple): 77 | #first 78 | 79 | Execute (all fields of 'Level' object should be complete): 80 | let actual = levels#MarkdownLevel() 81 | Assert len(actual) == 1 82 | AssertEqual actual[0].active, 1 83 | AssertEqual actual[0].level, 1 84 | AssertEqual actual[0].header, 'first' 85 | AssertEqual actual[0].fileLineNum, 1 86 | 87 | -------------------------------------------------------------------------------- /autoload/levels.vim: -------------------------------------------------------------------------------- 1 | let s:is = 0 2 | 3 | function! levels#MarkdownLevel() abort 4 | let l:outline = [] 5 | let l:i = 1 6 | while i <= line('$') 7 | let line = getline(i) 8 | if !IsFenced(i) 9 | call Header(l:outline, line, i) 10 | endif 11 | let l:i += 1 12 | endwhile 13 | 14 | if exists('g:markdrawer_toc') 15 | if g:markdrawer_toc ==# 'full_index' 16 | let l:c = [0, 0, 0, 0, 0, 0] 17 | let l:i = 0 18 | while i < len(l:outline) 19 | let l:x = l:outline[i].level - 1 20 | let l:c[l:x] = l:c[l:x] + 1 21 | let l:outline[i].index = l:c[l:x] 22 | let l:i += 1 23 | endwhile 24 | endif 25 | if g:markdrawer_toc ==# 'index' 26 | let l:c = [0, 0, 0, 0, 0, 0] 27 | let l:i = 0 28 | let l:prev = 0 29 | while i < len(l:outline) 30 | " if l:curr becomes less than prev, I reset that l:c[curr] 31 | let l:x = l:outline[i].level - 1 " this is curr 32 | if l:x < l:prev 33 | let l:c[l:prev] = 0 34 | let l:prev = 0 35 | endif 36 | let l:c[l:x] = l:c[l:x] + 1 37 | let l:outline[i].index = l:c[l:x] 38 | let l:prev = l:x 39 | let l:i += 1 40 | endwhile 41 | endif 42 | endif 43 | 44 | " TODO: Should be able to do this simply enough in the Header function 45 | let l:i = len(l:outline) - 1 46 | while i > -1 47 | if line('.') >= l:outline[i].fileLineNum 48 | let l:outline[i].active = 1 49 | let l:i = -1 50 | endif 51 | let l:i -= 1 52 | endwhile 53 | return l:outline 54 | endfunction 55 | 56 | function! HeaderName(line) abort 57 | let l:h = a:line 58 | if len(a:line) == 0 59 | let l:h = '' 60 | endif 61 | return l:h 62 | endfunction 63 | 64 | function! IsFenced(line) abort 65 | let syntaxGroup = map(synstack(a:line, 1), 'synIDattr(v:val, "name")') 66 | for value in syntaxGroup 67 | if value =~# '\vmarkdown(Code|Highlight)' 68 | return 1 69 | endif 70 | endfor 71 | return 0 72 | endfunction 73 | 74 | " TODO: don't pass in outline 75 | function! Header(outline, line, lNum) abort 76 | let l:pattern = '\v^\s*(#+)\s*(.*)' 77 | let l:matches = matchlist(a:line, l:pattern) 78 | 79 | let l:max_levels = get(g:, 'markdrawer_drawer_max_levels', 6) 80 | if len(l:matches) > 0 && len(l:matches[1]) <= l:max_levels 81 | call add(a:outline, {'fileLineNum': a:lNum, 'active': 0, 'level': len(l:matches[1]), 'header': HeaderName(l:matches[2]) }) 82 | endif 83 | endfunction 84 | -------------------------------------------------------------------------------- /autoload/ui.vim: -------------------------------------------------------------------------------- 1 | let s:drawerName = '__Markdown_Drawer__' 2 | let s:outline = [] 3 | let s:file = '' 4 | let s:fileLength = 0 5 | 6 | function! ui#OpenMarkdownDrawer() abort 7 | let s:file = expand('%:p') 8 | let s:fileLength = line('$') 9 | 10 | let s:outline = levels#MarkdownLevel() 11 | 12 | " Prevent multiple versions of the Drawer 13 | if !ReuseWindow() 14 | call CreateNewWindow() 15 | else 16 | execute bufwinnr(s:drawerName) . 'wincmd w' 17 | endif 18 | 19 | " Redraw outline 20 | setlocal noreadonly modifiable 21 | normal! ggdG 22 | call append(0, CreateTree()) 23 | normal! dd 24 | 25 | setlocal readonly nomodifiable 26 | 27 | let l:i = 0 28 | let l:goto = 0 29 | while i < len(s:outline) 30 | if 1 == s:outline[i].active 31 | let l:goto = i + 1 32 | endif 33 | let l:i += 1 34 | endwhile 35 | 36 | " removing all markers from first header does not set active flag 37 | if l:goto == 0 38 | let l:goto = 1 39 | endif 40 | 41 | execute 'normal! ' . l:goto . 'G' 42 | execute 'nnoremap '. g:markdrawerDelete . ' :call Delete()' 43 | execute 'nnoremap '. g:markdrawer_paste_below . ' :call PasteBelow()' 44 | execute 'nnoremap '. g:markdrawer_increase . ' :call Increase()' 45 | execute 'nnoremap '. g:markdrawer_decrease . ' :call Decrease()' 46 | execute 'nnoremap '. g:markdrawer_goto . ' :call GoTo()' 47 | 48 | endfunction 49 | 50 | function! CreateNewWindow() abort 51 | execute 'vsplit' s:drawerName 52 | execute 'vertical resize '. g:markdrawer_width 53 | set winfixwidth 54 | setlocal filetype=markdrawer 55 | setlocal buftype=nofile 56 | endfunction 57 | 58 | " if the drawer is already opened 59 | function! ReuseWindow() abort 60 | if bufwinnr(s:drawerName) != -1 61 | return 1 62 | endif 63 | return 0 64 | endfunction 65 | 66 | function! CreateTree() abort 67 | let l:list = [] 68 | for i in s:outline 69 | let l:h = repeat(g:markdrawer_prefix, i.level - 1) 70 | if exists('g:markdrawer_toc') 71 | let l:h = l:h . i.index . '. ' 72 | endif 73 | let l:h = l:h . i.header 74 | call add(l:list, l:h) 75 | endfor 76 | return l:list 77 | endfunction 78 | 79 | " refresh the tree after setting a new level 80 | function! ui#MarkDrawerLevelSet(args) abort 81 | if a:args =~# '[^0-9]' 82 | echom 'Not a number: ' . a:args 83 | return 84 | endif 85 | let g:markdrawer_drawer_max_levels=a:args 86 | call GoTo() 87 | call ui#OpenMarkdownDrawer() 88 | endfunction 89 | 90 | " Finds the where to place cursor base off the outline 91 | " and returns that line number 92 | function! GoTo() abort 93 | let l = s:outline[line('.') - 1].fileLineNum 94 | execute bufwinnr(s:file) . 'wincmd w' 95 | execute 'normal! ' . l . 'G' 96 | return l 97 | endfunction 98 | 99 | function! Delete() abort 100 | let l:i = line('.')-1 101 | let s:dStart = s:outline[i].fileLineNum 102 | let s:dEnd = s:fileLength 103 | 104 | syntax clear ToDelete 105 | execute 'syntax match ToDelete /\%' . line('.') . 'l.*/' 106 | 107 | if len(s:outline) > l:i + 1 108 | let s:dEnd = s:outline[l:i + 1].fileLineNum - 1 109 | endif 110 | endfunction 111 | 112 | function! PasteBelow() abort 113 | if exists('s:dStart') && exists('s:dEnd') 114 | let l:to = s:fileLength 115 | if len(s:outline) > line('.') 116 | let l:to = s:outline[line('.')].fileLineNum - 1 117 | endif 118 | 119 | execute bufwinnr(s:file) . 'wincmd w' 120 | 121 | " Move lines 122 | execute s:dStart . ',' . s:dEnd . 'm ' . l:to 123 | 124 | call ui#OpenMarkdownDrawer() 125 | syntax clear ToDelete 126 | endif 127 | endfunction 128 | 129 | function! Decrease() abort 130 | let l:l = GoTo() 131 | if matchend(getline(l:l), '\m\C^#\+') < 6 132 | execute 'normal! I#' 133 | endif 134 | call ui#OpenMarkdownDrawer() 135 | endfunction 136 | 137 | function! Increase() abort 138 | let l:l = GoTo() 139 | if matchend(getline(l:l), '\m\C^#\+') > 0 140 | execute 's/^#//' 141 | endif 142 | call ui#OpenMarkdownDrawer() 143 | endfunction 144 | --------------------------------------------------------------------------------