├── .gitignore ├── ftdetect └── d2.vim ├── assets ├── preview.gif └── replace.gif ├── compiler └── d2.vim ├── ci └── gen_syntax_d2_md.sh ├── plugin └── d2.vim ├── indent └── d2.vim ├── autoload ├── d2 │ ├── play.vim │ ├── fmt.vim │ ├── validate.vim │ └── ascii.vim └── d2.vim ├── LICENSE.txt ├── test ├── valid.d2 ├── syntax.d2 └── syntax.d2.md ├── ftplugin └── d2.vim ├── logo.svg ├── README.md ├── syntax └── d2.vim └── doc └── d2.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # See :helptags 2 | doc/tags 3 | -------------------------------------------------------------------------------- /ftdetect/d2.vim: -------------------------------------------------------------------------------- 1 | au BufNewFile,BufRead *.d2 setfiletype d2 2 | -------------------------------------------------------------------------------- /assets/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terrastruct/d2-vim/HEAD/assets/preview.gif -------------------------------------------------------------------------------- /assets/replace.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terrastruct/d2-vim/HEAD/assets/replace.gif -------------------------------------------------------------------------------- /compiler/d2.vim: -------------------------------------------------------------------------------- 1 | if exists('current_compiler') 2 | finish 3 | endif 4 | let current_compiler = 'd2c' 5 | 6 | if exists(':CompilerSet') != 2 7 | command -nargs=* CompilerSet setlocal 8 | endif 9 | 10 | CompilerSet makeprg=d2c 11 | -------------------------------------------------------------------------------- /ci/gen_syntax_d2_md.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | cd "$(dirname "$0")/.." 4 | 5 | syntax_d2="$(cat test/syntax.d2)" 6 | 7 | cat > test/syntax.d2.md < 9 | 10 | 11 | \`\`\`d2 12 | $syntax_d2 13 | \`\`\` 14 | EOF 15 | -------------------------------------------------------------------------------- /plugin/d2.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_d2') 2 | finish 3 | endif 4 | let g:loaded_d2 = 1 5 | 6 | call d2#add_default_block_string_syntaxes() 7 | call d2#sync_markdown_fenced_languages() 8 | 9 | augroup d2_syntax_post 10 | autocmd! 11 | autocmd Syntax d2 call d2#syntax_post() 12 | augroup END 13 | 14 | " Global command for D2 selection preview (works in any file) 15 | command! -range D2PreviewSelection ,call d2#ascii#PreviewSelection() 16 | 17 | " Global command for replacing D2 selection with ASCII render (works in any file) 18 | command! -range D2ReplaceSelection ,call d2#ascii#ReplaceSelection() 19 | 20 | " Global command for copying ASCII preview (works in any file) 21 | command! D2PreviewCopy call d2#ascii#CopyPreview() 22 | 23 | " Global mapping for D2 selection preview 24 | vnoremap d2 :D2PreviewSelection 25 | 26 | " Global mapping for replacing D2 selection with ASCII render 27 | vnoremap rd2 :D2ReplaceSelection 28 | 29 | " Global mapping for copying ASCII preview to clipboard and yank register 30 | nnoremap yd2 :D2PreviewCopy 31 | -------------------------------------------------------------------------------- /indent/d2.vim: -------------------------------------------------------------------------------- 1 | if exists('b:did_indent') 2 | finish 3 | endif 4 | let b:did_indent = 1 5 | 6 | setlocal indentexpr=D2Indent(v:lnum) 7 | setlocal indentkeys=!^F,o,O,0},0],\| 8 | 9 | function! D2Indent(ln) abort 10 | const l:pln = prevnonblank(a:ln-1) 11 | let l:indent = indent(l:pln) 12 | 13 | if getline(a:ln-2) !~ '\\$' && getline(a:ln-1) =~ '\\$' 14 | " Opening line continuation indents. 15 | let l:indent += shiftwidth() 16 | endif 17 | 18 | if getline(l:pln-1) =~ '\\$' && getline(l:pln) !~ '\\$' 19 | " Closing line continuation deindents. 20 | let l:indent -= shiftwidth() 21 | endif 22 | 23 | if getline(l:pln) =~ '|`\+[^|`[:space:]]*\s*$' 24 | " Opening |` indents. 25 | let l:indent += shiftwidth() 26 | endif 27 | 28 | if getline(a:ln) =~ '^\s*`\+|' 29 | " Closing `| deindents. 30 | let l:indent -= shiftwidth() 31 | endif 32 | 33 | if getline(l:pln) =~ '[{[]\s*' . '\%(#.*\)\?$' 34 | " Opening { or [ indents. 35 | let l:indent += shiftwidth() 36 | endif 37 | 38 | if getline(a:ln) =~ '^\s*[}\]]' 39 | " Closing } or ] deindents. 40 | let l:indent -= shiftwidth() 41 | endif 42 | 43 | if l:indent < 0 44 | return 0 45 | endif 46 | return l:indent 47 | endfunction 48 | -------------------------------------------------------------------------------- /autoload/d2/play.vim: -------------------------------------------------------------------------------- 1 | " d2#play#Play opens the current buffer in D2 playground 2 | function! d2#play#Play() abort 3 | " Save current buffer to a temporary file 4 | let l:tmpname = tempname() . '.d2' 5 | silent execute 'write! ' . fnameescape(l:tmpname) 6 | 7 | " Build d2 play command with options 8 | let l:cmd = get(g:, 'd2_play_command', 'd2 play') 9 | 10 | " Add theme option 11 | let l:theme = get(g:, 'd2_play_theme', 0) 12 | let l:cmd .= ' --theme=' . l:theme 13 | 14 | " Add sketch option if enabled 15 | let l:sketch = get(g:, 'd2_play_sketch', 0) 16 | if l:sketch 17 | let l:cmd .= ' --sketch' 18 | endif 19 | 20 | " Add the file 21 | let l:cmd .= ' ' . shellescape(l:tmpname) 22 | 23 | " Execute the command 24 | let l:output = system(l:cmd) 25 | let l:exit_code = v:shell_error 26 | 27 | " Clean up temp file 28 | call delete(l:tmpname) 29 | 30 | " Handle result 31 | if l:exit_code != 0 32 | echohl ErrorMsg 33 | echo 'd2 play failed: ' . substitute(l:output, '\n$', '', '') 34 | echohl None 35 | else 36 | " d2 play outputs "info: opening playground: " on success 37 | if l:output =~ 'opening playground:' 38 | echo 'D2 file opened in playground' 39 | else 40 | echo 'D2 play executed successfully' 41 | endif 42 | endif 43 | endfunction 44 | 45 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2022 Terrastruct, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are 4 | permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of 7 | conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of 10 | conditions and the following disclaimer in the documentation and/or other materials 11 | provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors may be used 14 | to endorse or promote products derived from this software without specific prior written 15 | permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 18 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 20 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 21 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 23 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 24 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /autoload/d2/fmt.vim: -------------------------------------------------------------------------------- 1 | " d2#fmt#Format formats the current buffer using d2 fmt 2 | function! d2#fmt#Format() abort 3 | " Don't format if buffer is not modifiable 4 | if !&modifiable 5 | return 6 | endif 7 | 8 | " Save current view 9 | let l:view = winsaveview() 10 | 11 | " Save current buffer to a temporary file 12 | let l:tmpname = tempname() . '.d2' 13 | silent execute 'keepalt write! ' . fnameescape(l:tmpname) 14 | 15 | " Run d2 fmt 16 | let l:cmd = get(g:, 'd2_fmt_command', 'd2 fmt') . ' ' . shellescape(l:tmpname) 17 | let l:output = system(l:cmd) 18 | let l:exit_code = v:shell_error 19 | 20 | " Handle errors 21 | if l:exit_code != 0 22 | call delete(l:tmpname) 23 | if !get(g:, 'd2_fmt_fail_silently', 0) 24 | echohl ErrorMsg 25 | echom 'd2 fmt failed: ' . substitute(l:output, '\n$', '', '') 26 | echohl None 27 | endif 28 | return 29 | endif 30 | 31 | " Get the current buffer content for comparison 32 | let l:current = getline(1, '$') 33 | 34 | " Read the formatted file 35 | let l:formatted = readfile(l:tmpname) 36 | 37 | " Clean up temp file 38 | call delete(l:tmpname) 39 | 40 | " Only update if content changed 41 | if l:formatted != l:current 42 | " Preserve undo history 43 | let l:undofile = &undofile 44 | let l:undodir = &undodir 45 | try 46 | set noundofile 47 | 48 | " Replace buffer content 49 | let l:modified_save = &modified 50 | silent! undojoin 51 | silent execute '1,$delete _' 52 | call setline(1, l:formatted) 53 | 54 | " Preserve modified state 55 | if !l:modified_save 56 | setlocal nomodified 57 | endif 58 | finally 59 | " Restore undo settings 60 | let &undofile = l:undofile 61 | let &undodir = l:undodir 62 | endtry 63 | 64 | " Force syntax sync 65 | syntax sync fromstart 66 | endif 67 | 68 | " Restore view 69 | call winrestview(l:view) 70 | endfunction 71 | 72 | " d2#fmt#ToggleAutoFormat toggles the auto format on save setting 73 | function! d2#fmt#ToggleAutoFormat() abort 74 | let g:d2_fmt_autosave = !get(g:, 'd2_fmt_autosave', 1) 75 | if g:d2_fmt_autosave 76 | echo "D2 auto format on save: enabled" 77 | else 78 | echo "D2 auto format on save: disabled" 79 | endif 80 | endfunction -------------------------------------------------------------------------------- /autoload/d2/validate.vim: -------------------------------------------------------------------------------- 1 | " d2#validate#Validate validates the current buffer using d2 validate 2 | function! d2#validate#Validate() abort 3 | " Save current buffer to a temporary file 4 | let l:tmpname = tempname() . '.d2' 5 | silent execute 'write! ' . fnameescape(l:tmpname) 6 | 7 | " Run d2 validate 8 | let l:cmd = get(g:, 'd2_validate_command', 'd2 validate') . ' ' . shellescape(l:tmpname) 9 | let l:output = system(l:cmd) 10 | let l:exit_code = v:shell_error 11 | 12 | " Clean up temp file 13 | call delete(l:tmpname) 14 | 15 | " Clear previous errors 16 | let l:list_type = get(g:, 'd2_list_type', 'quickfix') 17 | if l:list_type == 'locationlist' 18 | call setloclist(0, [], 'r') 19 | else 20 | call setqflist([], 'r') 21 | endif 22 | 23 | " If validation passed, notify and return 24 | if l:exit_code == 0 25 | echo "D2 validation passed" 26 | return 27 | endif 28 | 29 | " Parse errors from output 30 | let l:errors = [] 31 | let l:filename = expand('%:p') 32 | 33 | " D2 error format: "err: [path:] line:col: message" 34 | for l:line in split(l:output, '\n') 35 | if l:line =~ '^err:' 36 | let l:parts = matchlist(l:line, '^err:\s*\%(.*:\s*\)\?\(\d\+\):\(\d\+\):\s*\(.*\)') 37 | if len(l:parts) > 3 38 | call add(l:errors, { 39 | \ 'filename': l:filename, 40 | \ 'lnum': str2nr(l:parts[1]), 41 | \ 'col': str2nr(l:parts[2]), 42 | \ 'text': l:parts[3], 43 | \ 'type': 'E' 44 | \ }) 45 | endif 46 | endif 47 | endfor 48 | 49 | " Populate error list 50 | if !empty(l:errors) 51 | if l:list_type == 'locationlist' 52 | call setloclist(0, l:errors, 'r') 53 | if !get(g:, 'd2_validate_fail_silently', 0) 54 | lopen 55 | endif 56 | else 57 | call setqflist(l:errors, 'r') 58 | if !get(g:, 'd2_validate_fail_silently', 0) 59 | copen 60 | endif 61 | endif 62 | else 63 | " If we couldn't parse errors, show the raw output 64 | echohl ErrorMsg 65 | echo "D2 validation failed: " . substitute(l:output, '\n', ' ', 'g') 66 | echohl None 67 | endif 68 | endfunction 69 | 70 | " d2#validate#ToggleAutoValidate toggles the auto validate on save setting 71 | function! d2#validate#ToggleAutoValidate() abort 72 | let g:d2_validate_autosave = !get(g:, 'd2_validate_autosave', 0) 73 | if g:d2_validate_autosave 74 | echo "D2 auto validate on save: enabled" 75 | else 76 | echo "D2 auto validate on save: disabled" 77 | endif 78 | endfunction -------------------------------------------------------------------------------- /test/valid.d2: -------------------------------------------------------------------------------- 1 | vars: { 2 | d2-config: { 3 | theme-id: 3 # terrastruct 4 | sketch: true 5 | layout-engine: elk 6 | } 7 | colors: { 8 | c2: "#C7F1FF" # light turkuaz 9 | c3: "#B5AFF6" # dark purple 10 | c4: "#DEE1EB" # gray 11 | c5: "#88DCF7" # turkuaz 12 | c6: "#E4DBFE" # purple 13 | } 14 | } 15 | 16 | bank: { 17 | style.fill: white 18 | Corporate: { 19 | style.fill: white 20 | app14506: Data Source\ntco: 100,000\nowner: Lakshmi { 21 | style: { 22 | fill: '#fce7c6' 23 | } 24 | } 25 | } 26 | } 27 | bank.Risk.app14490 -> bank.Equities.app14527: client master 28 | 29 | k: { 30 | shape: sequence_diagram 31 | 32 | backend_server 33 | local_server 34 | ssh_server 35 | other_server 36 | 37 | 启动阶段: { 38 | local_server -> ssh_server: ssh_login 39 | local_server <- ssh_server: ssh 登录成功 40 | local_server -> ssh_server: sftp_copy 辅助转发服务器 41 | local_server -> ssh_server: ssh 启动转发服务器 42 | } 43 | } 44 | 45 | LangUnits: { 46 | style.fill: ${colors.c6} 47 | RegexVal: { 48 | ds 49 | } 50 | SQLSelect: { 51 | ds 52 | } 53 | PythonTr: { 54 | ds 55 | } 56 | langunit ₙ: { 57 | style.multiple: true 58 | style.stroke-dash: 10 59 | style.stroke: black 60 | style.animated: 1 61 | "... ds" 62 | } 63 | } 64 | 65 | Dataset UI: { 66 | style.fill: ${colors.c4} 67 | } 68 | 69 | *.style.font-size: 22 70 | *.*.style.font-size: 22 71 | 72 | title: |md 73 | # Terraform resources (v1.0.0) 74 | | {near: top-center} 75 | 76 | direction: right 77 | 78 | github_actions: GitHub Actions { 79 | lambda_action: Lambda Action { 80 | icon: https://icons.terrastruct.com/dev%2Fgithub.svg 81 | style.multiple: true 82 | } 83 | style: { 84 | stroke: blue 85 | font-color: purple 86 | stroke-dash: 3 87 | fill: white 88 | } 89 | } 90 | 91 | explanation: |md 92 | ```yaml 93 | deploy_source: 94 | name: deploy lambda from source 95 | runs-on: ubuntu-latest 96 | steps: 97 | - name: checkout source code 98 | uses: actions/checkout@v3 99 | - name: default deploy 100 | uses: appleboy/lambda-action@v0.1.7 101 | with: 102 | aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} 103 | aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 104 | aws_region: ${{ secrets.AWS_REGION }} 105 | function_name: gorush 106 | source: example/index.js 107 | ``` 108 | | {near: bottom-center} 109 | 110 | task01: { 111 | icon: https://icons.terrastruct.com/essentials%2F092-graph%20bar.svg 112 | class: [task; multiple] 113 | } 114 | -------------------------------------------------------------------------------- /test/syntax.d2: -------------------------------------------------------------------------------- 1 | first: val 2 | # test for syntax/d2.vim 3 | 4 | x: 1, y: 2 5 | 6 | x: null 7 | x: null #comment 8 | x: true 9 | x: true3 10 | x: false 11 | x: xfalse 12 | x: 5e3 13 | x: 123. 14 | x: meow234 15 | imports: mdkalsmdksal 16 | imports343: mdkalsmdksal 17 | 18 | unquoted_str: hello 19 | unquoted_str_err: hello 20 | unquoted_str_escape: \ 21 | 3434343 22 | single_quote_escapes: 'testing\ 23 | before''dsdsafter\' 24 | double_quote_escapes: "\"damdkslamadkls\" dmasldkasl" 25 | 26 | 'one''two'.three -> xd 27 | 28 | # nested-markdown-block-string-test 29 | x -> y 30 | y: |||md 31 | # d2-vim 32 | The Vim plugin for [D2](https://d2lang.com) files. 33 | ```d2 34 | x -> y 35 | y: ||md 36 | # d2-vim 37 | The Vim plugin for [D2](https://d2lang.com) files. 38 | ```d2 39 | x -> y 40 | y: |md 41 | # d2-vim 42 | The Vim plugin for [D2](https://d2lang.com) files. 43 | | 44 | ``` 45 | || 46 | ``` 47 | ||| 48 | 49 | my_code: |`ts 50 | declare function getSmallPet(): Fish | Bird; 51 | const works = (a > 1) || (b < 2) 52 | `| 53 | 54 | x: |html hello world | 55 | 56 | x: [1,2,3] 57 | x: [first,second,third] 58 | 59 | x: ${ok} 60 | 61 | ok: null less 62 | ok: something 63 | 64 | (x -> y)[34]: label 65 | o*ne&err ** &three -> &work: label 66 | err: 232 false [ 67 | err: ] 68 | 69 | double_map_err: dbl {} {} 70 | string_after_map_err: {} "dsamkdlas" 71 | 72 | subst: some body once told me ${wsup} on a good friday morning 73 | subst: ${wsup} on a good friday morning 74 | subst_err: $dmaslkdas 75 | 76 | &(x -> y)[0]: should work 77 | &class: *match* 78 | 79 | key_val_err: dasmkdasm ] dmlkas 80 | 81 | arr: [${yes}] 82 | 83 | one,\ 84 | two: three 85 | 86 | two_err\ 87 | &two 88 | two_err.\ 89 | &two 90 | &dsamkldaskl 91 | 92 | \ 93 | err_sc 94 | esc \ 95 | esc 96 | 97 | ok_err: esc \ 98 | [arr] 99 | wow.\ 100 | &ok 101 | wow_err: dsamdlas \ 102 | { dmaklsmdakls } 103 | "ok" --\ 104 | -- dasklmmdklas 105 | "ok_err" -\x 106 | -- dasklmmdklas 107 | 108 | ok: \ 109 | {} 110 | ok: \ 111 | [] 112 | ok: \ 113 | [] 114 | 115 | meow --> \ 116 | "dmlsakdksmal" 117 | 118 | err. 119 | .err 120 | err: { .err } 121 | 122 | ok.*.*: yes 123 | 124 | ok.s\ 125 | .ok: mlkdasmdlkas 126 | ok_err.s \ 127 | .ok: mlkdasmdlkas 128 | 129 | \ 130 | single_slash_err 131 | 132 | x\\: ok 133 | & --> ok 134 | bad -- -- 135 | 136 | ok: { 137 | Static resources."icons.terrastruct.com" --\ 138 | -> Frontend."app.terrastruct.com" 139 | 2 --\ 140 | 2-> 2 141 | } 142 | 143 | *: { 144 | &shape: person 145 | &connected: true 146 | style.fill: red 147 | } 148 | (** -> **)[*]: { 149 | &src: a 150 | &dst: c 151 | style.stroke: yellow 152 | } 153 | a -> b 154 | a.shape: person 155 | a -> c 156 | -------------------------------------------------------------------------------- /test/syntax.d2.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ```d2 5 | first: val 6 | # test for syntax/d2.vim 7 | 8 | x: 1, y: 2 9 | 10 | x: null 11 | x: null #comment 12 | x: true 13 | x: true3 14 | x: false 15 | x: xfalse 16 | x: 5e3 17 | x: 123. 18 | x: meow234 19 | imports: mdkalsmdksal 20 | imports343: mdkalsmdksal 21 | 22 | unquoted_str: hello 23 | unquoted_str_err: hello `mdlkamdskl` 24 | unquoted_str_escape: \ 25 | 3434343 26 | single_quote_escapes: 'testing\ 27 | before''dsdsafter\' 28 | double_quote_escapes: "\"damdkslamadkls\" dmasldkasl" 29 | 30 | 'one''two'.three -> xd 31 | "err""2".3 -> xd 32 | 33 | # nested-markdown-block-string-test 34 | x -> y 35 | y: |`md 36 | # d2-vim 37 | The vim plugin for [Terrastruct](https://terrastruct.com)'s declarative diagramming .d2 files. 38 | ```d2 39 | x -> y 40 | y: |`md 41 | # d2-vim 42 | The vim plugin for [Terrastruct](https://terrastruct.com)'s declarative diagramming .d2 files. 43 | ```d2 44 | x -> y 45 | y: |`md 46 | # d2-vim 47 | The vim plugin for [Terrastruct](https://terrastruct.com)'s declarative diagramming .d2 files. 48 | `| 49 | ``` 50 | `| 51 | ``` 52 | `| 53 | 54 | x -> y 55 | y: |`d2 56 | x -> y 57 | y: |`d2 58 | x -> y 59 | y: |`d2 60 | x -> y 61 | y: |`d2 62 | `| 63 | `| 64 | `| 65 | `| 66 | 67 | x: |`html hello world `| 68 | 69 | x: [1,2,3] 70 | x: [first,second,third] 71 | 72 | x: ${ok} 73 | 74 | oneline: |`'!@#$%^&*()testing bark`| 75 | oneline: |`'!@#$%^&*()testing bark `| 76 | oneline_err: |`'!@#$%^&*()testing|bark `| 77 | 78 | ok: null less 79 | ok: something 80 | 81 | (x -> y)[34]: label 82 | o*ne&err ** &three -> &work: label 83 | err: 232 false [ 84 | err: ] 85 | 86 | double_map_err: dbl {} {} 87 | string_after_map_err: {} "dsamkdlas" 88 | 89 | subst: some body once told me ${wsup} on a good friday morning 90 | subst: ${wsup} on a good friday morning 91 | subst_err: $dmaslkdas 92 | 93 | &(x -> y)[0]: should work 94 | &class: *match* 95 | 96 | key_val_err: dasmkdasm ] dmlkas 97 | 98 | arr: [${yes}] 99 | 100 | one,\ 101 | two: three 102 | 103 | two_err\ 104 | &two 105 | two_err.\ 106 | &two 107 | &dsamkldaskl 108 | 109 | \ 110 | err_sc 111 | esc \ 112 | esc 113 | 114 | ok_err: esc \ 115 | [arr] 116 | wow.\ 117 | &ok 118 | wow_err: dsamdlas \ 119 | { dmaklsmdakls } 120 | "ok" --\ 121 | -- dasklmmdklas 122 | "ok_err" -\x 123 | -- dasklmmdklas 124 | 125 | ok: \ 126 | {} 127 | ok: \ 128 | [] 129 | ok: \ 130 | [] 131 | 132 | meow --> \ 133 | "dmlsakdksmal" 134 | 135 | err. 136 | .err 137 | err: { .err } 138 | 139 | ok.*.*: yes 140 | 141 | ok.s\ 142 | .ok: mlkdasmdlkas 143 | ok_err.s \ 144 | .ok: mlkdasmdlkas 145 | 146 | \ 147 | single_slash_err 148 | 149 | x\\: ok 150 | & --> ok 151 | bad -- -- 152 | 153 | ok: { 154 | Static resources."icons.terrastruct.com" --\ 155 | -> Frontend."app.terrastruct.com" 156 | 2 --\ 157 | 2-> 2 158 | } 159 | ``` 160 | -------------------------------------------------------------------------------- /ftplugin/d2.vim: -------------------------------------------------------------------------------- 1 | if exists('b:did_ftplugin') 2 | finish 3 | endif 4 | let b:did_ftplugin = 1 5 | 6 | " d2 files indent with 2 spaces. 7 | setlocal expandtab 8 | setlocal tabstop=2 9 | setlocal shiftwidth=0 10 | setlocal softtabstop=0 11 | 12 | " Disable autowrapping. 13 | setlocal formatoptions-=t 14 | 15 | setlocal comments=b:# 16 | let &l:commentstring = '# %s' 17 | 18 | " Format on save configuration 19 | if !exists("g:d2_fmt_autosave") 20 | let g:d2_fmt_autosave = 1 21 | endif 22 | 23 | if !exists("g:d2_fmt_command") 24 | let g:d2_fmt_command = "d2 fmt" 25 | endif 26 | 27 | if !exists("g:d2_fmt_fail_silently") 28 | let g:d2_fmt_fail_silently = 0 29 | endif 30 | 31 | " Auto format on save 32 | augroup d2_ftplugin 33 | autocmd! 34 | if get(g:, "d2_fmt_autosave", 1) 35 | autocmd BufWritePre *.d2 call d2#fmt#Format() 36 | endif 37 | augroup END 38 | 39 | " Validation configuration 40 | if !exists("g:d2_validate_autosave") 41 | let g:d2_validate_autosave = 0 42 | endif 43 | 44 | if !exists("g:d2_validate_command") 45 | let g:d2_validate_command = "d2 validate" 46 | endif 47 | 48 | if !exists("g:d2_validate_fail_silently") 49 | let g:d2_validate_fail_silently = 0 50 | endif 51 | 52 | if !exists("g:d2_list_type") 53 | let g:d2_list_type = "quickfix" 54 | endif 55 | 56 | " Play configuration 57 | if !exists("g:d2_play_command") 58 | let g:d2_play_command = "d2 play" 59 | endif 60 | 61 | if !exists("g:d2_play_theme") 62 | let g:d2_play_theme = 0 63 | endif 64 | 65 | if !exists("g:d2_play_sketch") 66 | let g:d2_play_sketch = 0 67 | endif 68 | 69 | " ASCII render configuration 70 | if !exists("g:d2_ascii_command") 71 | let g:d2_ascii_command = "d2" 72 | endif 73 | 74 | if !exists("g:d2_ascii_preview_width") 75 | let g:d2_ascii_preview_width = &columns / 2 76 | endif 77 | 78 | if !exists("g:d2_ascii_autorender") 79 | let g:d2_ascii_autorender = 1 80 | endif 81 | 82 | if !exists("g:d2_ascii_mode") 83 | let g:d2_ascii_mode = "extended" 84 | endif 85 | 86 | " Auto validate on save (runs after formatting) 87 | augroup d2_validate 88 | autocmd! 89 | if get(g:, "d2_validate_autosave", 0) 90 | autocmd BufWritePost *.d2 call d2#validate#Validate() 91 | endif 92 | augroup END 93 | 94 | " Auto ASCII render on save 95 | augroup d2_ascii 96 | autocmd! 97 | if get(g:, "d2_ascii_autorender", 1) 98 | autocmd BufWritePost *.d2 call d2#ascii#PreviewUpdate() 99 | endif 100 | augroup END 101 | 102 | " Commands 103 | command! -buffer D2Fmt call d2#fmt#Format() 104 | command! -buffer D2FmtToggle call d2#fmt#ToggleAutoFormat() 105 | command! -buffer D2Validate call d2#validate#Validate() 106 | command! -buffer D2ValidateToggle call d2#validate#ToggleAutoValidate() 107 | command! -buffer D2Play call d2#play#Play() 108 | command! -buffer D2Preview call d2#ascii#Preview() 109 | command! -buffer D2PreviewToggle call d2#ascii#PreviewToggle() 110 | command! -buffer D2PreviewUpdate call d2#ascii#PreviewUpdate() 111 | command! -buffer D2AsciiToggle call d2#ascii#ToggleAutoRender() 112 | 113 | " Normal mode mapping for D2 files - preview entire buffer 114 | nnoremap d2 :D2Preview 115 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | D2 4 |

5 | 6 | The Vim plugin for [D2](https://d2lang.com) files. 7 |
8 |
9 |
10 | 11 | ## Table of Contents 12 | 13 | - [Install](#install) 14 | - [Features](#features) 15 | - [Syntax Highlighting](#syntax-highlighting) 16 | - [ASCII Preview](#ascii-preview) 17 | - [Auto-formatting](#auto-formatting) 18 | - [Validation](#validation) 19 | - [Playground](#playground) 20 | - [Documentation](#documentation) 21 | 22 | ## Install 23 | 24 | ### Using [vim-plug](https://github.com/junegunn/vim-plug) 25 | ```vim 26 | Plug 'terrastruct/d2-vim' 27 | ``` 28 | 29 | ### Using [lazy.nvim](https://github.com/folke/lazy.nvim) 30 | ```lua 31 | { 32 | "terrastruct/d2-vim", 33 | ft = { "d2" }, 34 | } 35 | ``` 36 | 37 | ## Features 38 | 39 | ### Syntax Highlighting 40 | 41 | This plugin provides basic syntax highlighting for D2 files. However, for Neovim users, we recommend using the Tree-sitter parser for more accurate syntax highlighting: 42 | 43 | **[ravsii/tree-sitter-d2](https://github.com/ravsii/tree-sitter-d2)** 44 | 45 | It is well-tested and maintained (thank you [@ravsii](https://github.com/ravsii)). 46 | 47 | The plugin standalone will still be used as a fallback for regular Vim users or when Tree-sitter is not available. 48 | 49 | ### ASCII Preview 50 | 51 | **Requirements:** D2 version 0.7.1 or higher is required for ASCII features. 52 | 53 | Render D2 diagrams as ASCII text for quick preview without leaving Vim. This feature provides a live preview of your diagrams in text format, perfect for: 54 | - Quick previews without external tools 55 | - Working in terminal environments 56 | - Sharing diagrams in text-only contexts 57 | - Understanding diagram structure while editing 58 | 59 | The ASCII preview opens in a vertical split pane and automatically updates when you save your D2 file. 60 | 61 | #### Demos 62 | 63 | **Preview**: Open a `.d2` file, press `d2` to open up preview pane, upon save, the ascii 64 | re-renders. 65 | 66 | ![ASCII Preview Demo](assets/preview.gif) 67 | 68 | **Replace**: Open any file (here we have a Go file), comment some d2 code, select it in visual mode, press `rd2` to replace the d2 code with an ASCII diagram. 69 | 70 | ![Replace Demo](assets/replace.gif) 71 | 72 | #### Configuration 73 | 74 | ```vim 75 | " Enable/disable auto ASCII render on save (default: 1) 76 | let g:d2_ascii_autorender = 1 77 | 78 | " Customize the ASCII render command (default: "d2") 79 | let g:d2_ascii_command = "d2" 80 | 81 | " Set preview window width for vertical split (default: half screen) 82 | let g:d2_ascii_preview_width = &columns / 2 83 | 84 | " Set ASCII mode: "extended" (Unicode) or "standard" (basic ASCII) 85 | let g:d2_ascii_mode = "extended" 86 | ``` 87 | 88 | #### ASCII Modes 89 | 90 | **Extended Mode (default)**: Uses Unicode box-drawing characters for cleaner, more readable output: 91 | ``` 92 | ┌─────────────┐ ┌──────────────┐ 93 | │ user │────▶│ server │ 94 | └─────────────┘ └──────────────┘ 95 | ``` 96 | 97 | **Standard Mode**: Uses basic ASCII characters for maximum compatibility: 98 | ``` 99 | +-------------+ +--------------+ 100 | | user |---->| server | 101 | +-------------+ +--------------+ 102 | ``` 103 | 104 | #### Commands 105 | - `:D2Preview` - Render current buffer as ASCII in preview window 106 | - `:D2PreviewToggle` - Toggle ASCII preview window on/off 107 | - `:D2PreviewUpdate` - Update existing preview window with current content 108 | - `:D2PreviewCopy` - Copy ASCII preview content to clipboard and yank register 109 | - `:D2PreviewSelection` - Render selected text as ASCII (works in any file) 110 | - `:D2ReplaceSelection` - Replace selected D2 code with ASCII render (works in any file) 111 | - `:D2AsciiToggle` - Toggle automatic ASCII rendering on save 112 | 113 | #### Keybindings 114 | - `d2` - Render selected text as ASCII (visual mode, any file) 115 | - `d2` - Render entire buffer as ASCII (normal mode, D2 files only) 116 | - `rd2` - Replace selected D2 code with ASCII render (visual mode, any file) 117 | - `yd2` - Copy ASCII preview content to clipboard and yank register (normal mode, any file) 118 | 119 | ### Auto-formatting 120 | D2 files are automatically formatted on save using `d2 fmt`. This can be configured: 121 | 122 | ```vim 123 | " Enable/disable auto format on save (default: 1) 124 | let g:d2_fmt_autosave = 1 125 | 126 | " Customize the format command (default: "d2 fmt") 127 | let g:d2_fmt_command = "d2 fmt" 128 | 129 | " Fail silently when formatting fails (default: 0) 130 | let g:d2_fmt_fail_silently = 0 131 | ``` 132 | 133 | Commands: 134 | - `:D2Fmt` - Format current buffer 135 | - `:D2FmtToggle` - Toggle auto format on save 136 | 137 | ### Validation 138 | D2 files can be validated using `d2 validate`. This can be configured: 139 | 140 | ```vim 141 | " Enable/disable auto validate on save (default: 0) 142 | let g:d2_validate_autosave = 0 143 | 144 | " Customize the validate command (default: "d2 validate") 145 | let g:d2_validate_command = "d2 validate" 146 | 147 | " Use quickfix or locationlist for errors (default: "quickfix") 148 | let g:d2_list_type = "quickfix" 149 | 150 | " Fail silently when validation fails (default: 0) 151 | let g:d2_validate_fail_silently = 0 152 | ``` 153 | 154 | Commands: 155 | - `:D2Validate` - Validate current buffer 156 | - `:D2ValidateToggle` - Toggle auto validate on save 157 | 158 | ### Playground 159 | Open D2 files in the online playground at [play.d2lang.com](https://play.d2lang.com). This 160 | is useful for an ad-hoc way of sharing your d2 diagram with someone. 161 | 162 | ```vim 163 | " Customize the play command (default: "d2 play") 164 | let g:d2_play_command = "d2 play" 165 | 166 | " Set the theme ID (default: 0) 167 | let g:d2_play_theme = 0 168 | 169 | " Enable sketch mode (default: 0) 170 | let g:d2_play_sketch = 0 171 | ``` 172 | 173 | Commands: 174 | - `:D2Play` - Open current buffer in D2 playground 175 | 176 | ## Documentation 177 | 178 | See `:help d2-vim` or [./doc/d2.txt](./doc/d2.txt) for options and additional documentation. 179 | -------------------------------------------------------------------------------- /autoload/d2.vim: -------------------------------------------------------------------------------- 1 | if exists('s:loaded_d2') 2 | finish 3 | endif 4 | let s:loaded_d2 = 1 5 | 6 | function! d2#add_default_block_string_syntaxes() abort 7 | let g:d2_block_string_syntaxes = get(g:, 'd2_block_string_syntaxes', {}) 8 | if type(g:d2_block_string_syntaxes) != v:t_dict && empty(g:d2_block_string_syntaxes) 9 | return 10 | endif 11 | 12 | let g:d2_block_string_syntaxes = extend(g:d2_block_string_syntaxes, { 13 | \ 'd2': extend(['d2'], get(g:d2_block_string_syntaxes, 'd2', [])), 14 | \ 'markdown': extend(['md', 'markdown'], get(g:d2_block_string_syntaxes, 'markdown', [])), 15 | \ 'javascript': extend(['javascript', 'js'], get(g:d2_block_string_syntaxes, 'javascript', [])), 16 | \ 'html': extend(['html'], get(g:d2_block_string_syntaxes, 'html', [])), 17 | \ 'json': extend(['json'], get(g:d2_block_string_syntaxes, 'json', [])), 18 | \ 'c': extend(['c'], get(g:d2_block_string_syntaxes, 'c', [])), 19 | \ 'go': extend(['go'], get(g:d2_block_string_syntaxes, 'go', [])), 20 | \ 'sh': extend(['sh', 'ksh', 'bash'], get(g:d2_block_string_syntaxes, 'sh', [])), 21 | \ 'css': extend(['css'], get(g:d2_block_string_syntaxes, 'css', [])), 22 | \ 'vim': extend(['vim'], get(g:d2_block_string_syntaxes, 'vim', [])), 23 | \ }) 24 | endfunction 25 | 26 | function! d2#init_syn() abort 27 | syn case match 28 | syn sync fromstart 29 | endfunction 30 | 31 | function! d2#init_block_string_syntaxes() abort 32 | let b:included_syntaxes = get(b:, 'included_syntaxes', []) 33 | if exists('g:d2_block_string_syntaxes') == 0 || type(g:d2_block_string_syntaxes) != v:t_dict || empty(g:d2_block_string_syntaxes) == 1 34 | return 35 | endif 36 | 37 | for [l:syntax, l:tags] in items(g:d2_block_string_syntaxes) 38 | for l:tag in l:tags 39 | call d2#syn_block_string(l:syntax, l:tag) 40 | endfor 41 | endfor 42 | endfunction 43 | 44 | function! d2#syn_block_string(syntax, tag) abort 45 | let l:contains = '' 46 | let l:contained_group = 'd2BlockString' 47 | if a:syntax != '' 48 | let l:contained_group = 'd2BlockString'.substitute(a:syntax, '.', '\u\0', '') 49 | let l:contains = 'contains=@'.l:contained_group 50 | 51 | if a:syntax == 'markdown' && 52 | \ index(b:included_syntaxes, a:syntax) == -1 && 53 | \ get(g:, 'main_syntax', '') == 'markdown' 54 | " See comment on include_toplevel_markdown. 55 | call s:include_toplevel_markdown(l:contained_group) 56 | elseif a:syntax == 'd2' 57 | let l:contains = 'contains=TOP' 58 | else 59 | call s:include_syn(a:syntax, l:contained_group) 60 | endif 61 | endif 62 | 63 | " See nested-markdown-block-string-test for extend and keepend. We don't want parents or 64 | " children matching on our end pattern unless they too have extend and keepend set. i.e 65 | " recursive block strings. 66 | exe 'syn region '.l:contained_group.' matchgroup=d2BlockStringDelimiter start=/|'.a:tag.'\s*$/ end=/^|/ contained '.l:contains.' extend keepend' 67 | exe 'syn cluster d2BlockString add='.l:contained_group 68 | endfunction 69 | 70 | function! s:include_syn(syntax, contained_group) abort 71 | if index(b:included_syntaxes, a:syntax) != -1 72 | return 73 | endif 74 | if a:syntax == 'markdown' 75 | call d2#sync_markdown_fenced_languages() 76 | endif 77 | 78 | let b:included_syntaxes = add(b:included_syntaxes, a:syntax) 79 | 80 | exe 'syn include @'.a:contained_group.' syntax/'.a:syntax.'.vim' 81 | unlet b:current_syntax 82 | syn sync clear 83 | syn iskeyword clear 84 | call d2#init_syn() 85 | endfunction 86 | 87 | function! d2#sync_markdown_fenced_languages() abort 88 | if type(g:d2_block_string_syntaxes) != v:t_dict && empty(g:d2_block_string_syntaxes) 89 | return 90 | endif 91 | 92 | let g:markdown_fenced_languages = get(g:, 'markdown_fenced_languages', []) 93 | 94 | " Sync from g:d2_block_string_syntaxes to g:markdown_fenced_languages 95 | for [l:syntax, l:tags] in items(g:d2_block_string_syntaxes) 96 | for l:tag in l:tags 97 | if l:syntax == 'markdown' 98 | " markdown.vim isn't smart enough to not include itself. 99 | continue 100 | elseif l:syntax == 'vim' 101 | " markdown.vim is unable to embed the vim syntax. 102 | continue 103 | elseif l:syntax == 'd2' 104 | " Prevent circular dependency: markdown.vim -> d2.vim -> markdown.vim 105 | continue 106 | endif 107 | let l:line = l:tag 108 | if l:tag != l:syntax 109 | let l:line .= '='.l:syntax 110 | endif 111 | if index(g:markdown_fenced_languages, l:line) == -1 112 | let g:markdown_fenced_languages = add(g:markdown_fenced_languages, l:line) 113 | endif 114 | endfor 115 | endfor 116 | 117 | " Sync from g:markdown_fenced_languages to g:d2_block_string_syntaxes 118 | for l:syntax in g:markdown_fenced_languages 119 | let l:md_tag = matchstr(l:syntax, '^[^=]\+') 120 | let l:syntax = matchstr(l:syntax, '[^=]\+$') 121 | if l:syntax == '' 122 | let l:syntax = l:md_tag 123 | endif 124 | let l:bs_tags = get(g:d2_block_string_syntaxes, l:syntax, []) 125 | if index(l:bs_tags, l:md_tag) == -1 126 | let g:d2_block_string_syntaxes = extend(g:d2_block_string_syntaxes, { 127 | \ l:syntax: extend(l:bs_tags, [l:md_tag]), 128 | \ }) 129 | endif 130 | endfor 131 | endfunction 132 | 133 | function! d2#syntax_post() abort 134 | let b:included_syntaxes = get(b:, 'included_syntaxes', []) 135 | if index(b:included_syntaxes, 'markdown') != -1 136 | if hlexists('markdownCodeBlock') 137 | syn clear markdownCodeBlock 138 | endif 139 | 140 | syn cluster d2BlockStringMarkdown add=@markdownBlock 141 | endif 142 | endfunction 143 | 144 | " include_toplevel_markdown is called when markdown is the top level syntax and is 145 | " embedding us. We cannot directly include it as markdown.vim errors on being recursively 146 | " included but we can explicitly include its TOP groups except markdownCodeBlock. 147 | " See $VIMRUNTIME/syntax/markdown.vim 148 | function! s:include_toplevel_markdown(contained_group) abort 149 | syn cluster d2MarkdownBlock contains=markdownH1,markdownH2,markdownH3,markdownH4,markdownH5,markdownH6,markdownBlockquote,markdownListMarker,markdownOrderedListMarker,markdownRule 150 | syn match d2MarkdownLineStart "^[<@]\@!" nextgroup=@d2MarkdownBlock,htmlSpecialChar 151 | " Run :syn list @d2BlockStringMarkdown in a d2 file to see the TOP markdown groups. 152 | let l:contains = 'contains=markdownItalic,htmlError,htmlSpecialChar,htmlEndTag,htmlCssDefinition,htmlTag,htmlComment,htmlPreProc,htmlLink,javaScript,htmlStrike,htmlBold,htmlUnderline,htmlItalic,htmlH1,htmlH2,htmlH3,htmlH4,htmlH5,htmlH6,htmlTitle,cssStyle,htmlHead,markdownValid,d2MarkdownLineStart,markdownLineBreak,markdownLinkText,markdownBold,markdownCode,markdownEscape,markdownError,markdownAutomaticLink,markdownIdDeclaration,markdownBoldItalic,markdownFootnote,markdownFootnoteDefinition,@d2MarkdownBlock,@markdownInline' 153 | 154 | let g:markdown_fenced_languages = get(g:, 'markdown_fenced_languages', []) 155 | for l:syntax in g:markdown_fenced_languages 156 | let l:syntax = matchstr(l:syntax, '^[^=]\+') 157 | let l:contains .= ',markdownHighlight'.substitute(l:syntax, '\.', '', 'g') 158 | endfor 159 | 160 | exe 'syn cluster '.a:contained_group.' '.l:contains 161 | endfunction 162 | -------------------------------------------------------------------------------- /syntax/d2.vim: -------------------------------------------------------------------------------- 1 | if exists('b:current_syntax') 2 | finish 3 | endif 4 | 5 | call d2#init_syn() 6 | 7 | " This only marks errors where additional input after the error will not fix the error. 8 | " Thus it's not constantly marking things as errors while you type but only errors you 9 | " need to go back and deal with. 10 | syn region d2Error start=/\S/ end=/\%(\s*\n\)\@=/ 11 | 12 | " ******************************************************************************** 13 | " XXX: Caution 14 | " ******************************************************************************** 15 | " Very carefully written. Please keep changes in sync with d2-vscode. The regex 16 | " structure should map one to one. 17 | " 18 | " Keep the following delimiters in mind when making changes: 19 | " 20 | " top: \n#;[]{} 21 | " val: |`$'"\ 22 | " key: :.-<>*&() 23 | " 24 | " vals regex: [\n#;[\]{}|`$'"\\] 25 | " keys regex: [\n#;[\]{}|`$'"\\:.\-<>*&()] 26 | " 27 | " The main differences between Oniguruma and VimL regexes: 28 | " - Lookahead and lookbehind syntax 29 | " - Grouping syntax 30 | " - | + don't need to be escaped 31 | " - [ needs to be escaped in a character class 32 | " - parent group's have to use a lookahead if they want children to be able to match on 33 | " their start. 34 | 35 | " ******************************************************************************** 36 | " @d2String 37 | " ******************************************************************************** 38 | 39 | syn region d2StringUnquoted start=/\%([;[{:]\%(\s*\\\n\)\?\s*\|\\\@*&()]\@=/ end=/\%(\s*[\n#;[\]{}|`$'":.\-<>*&()]\)\@=/ contains=@d2EscapeKey 59 | syn region d2KeyQuotedSingle matchgroup=d2Delimiter start=/\%([;{.\->*&(]\%(\s*\\\n\)\?\s*\|\\\@*&(]\%(\s*\\\n\)\?\s*\|\\\@*&()]\)\@=/ 64 | 65 | hi def link d2KeyUnquoted d2Identifier 66 | hi def link d2KeyQuotedSingle d2Identifier 67 | hi def link d2KeyQuotedDouble d2Identifier 68 | hi def link d2KeyReserved d2Keyword 69 | 70 | syn cluster d2Key contains=d2KeyUnquoted,d2KeyQuotedSingle,d2KeyQuotedDouble,d2KeyGroup,d2KeyIndex,d2KeyReserved 71 | 72 | syn region d2KeyEdge start=/[\-<>]\+/ end=/\%([^\-<>]\|\n\)\@=/ 73 | syn match d2KeyPeriod /\%([^[:space:]\n#;[\]{}|`$:.\-<>&]\%(\s*\\\n\)\?\s*\)\@<=\./ 74 | syn match d2KeyGlob /\*/ 75 | syn match d2KeyDoubleGlob /\*\*/ 76 | syn match d2KeyAmpersand /\%([;{.]\%(\s*\\\n\)\?\s*\|\\\@*&()]*/ 94 | syn match d2Import /@[^[:space:]\n#;[\]{}|`$'"\\:\-<>*&()]*/ 95 | 96 | " ******************************************************************************** 97 | " @d2Escape 98 | " ******************************************************************************** 99 | " https://go.dev/ref/spec#String_literals 100 | " $VIMRUNTIME/syntax/go.vim 101 | 102 | syn match d2EscapeError /\\./ contained 103 | syn match d2EscapeSpecial /\\[#;[\]{}|`$'"\\]/ contained 104 | syn match d2EscapeSpecialKey /\\[:.\-<>*&()]/ contained 105 | syn match d2EscapeC /\\[abefnrtv]/ contained 106 | syn match d2EscapeX /\\x\x\{2}/ contained 107 | syn match d2EscapeOctal /\\[0-7]\{3}/ contained 108 | syn match d2EscapeU /\\u\x\{4}/ contained 109 | syn match d2EscapeBigU /\\U\x\{8}/ contained 110 | syn match d2LineContinuation /\%(\S\s*\)\@<=\\\n/ 111 | 112 | hi def link d2EscapeError d2Error 113 | hi def link d2EscapeSpecial d2Escape 114 | hi def link d2EscapeSpecialKey d2Escape 115 | hi def link d2EscapeC d2Escape 116 | hi def link d2EscapeX d2Escape 117 | hi def link d2EscapeOctal d2Escape 118 | hi def link d2EscapeU d2Escape 119 | hi def link d2EscapeBigU d2Escape 120 | hi def link d2LineContinuation d2Escape 121 | hi def link d2Escape d2SpecialChar 122 | 123 | syn cluster d2Escape contains=d2EscapeError,d2EscapeSpecial,d2EscapeC,d2EscapeX,d2EscapeOctal,d2EscapeU,d2EscapeBigU,d2LineContinuation 124 | syn cluster d2EscapeKey contains=@d2Escape,d2EscapeSpecialKey 125 | 126 | " ******************************************************************************** 127 | " d2Null 128 | " ******************************************************************************** 129 | 130 | syn match d2Null /null\%(\s*[\n#;[\]{}|`$'"\\]\)\@=/ contained 131 | hi def link d2Null d2Constant 132 | 133 | " ******************************************************************************** 134 | " d2Boolean 135 | " ******************************************************************************** 136 | 137 | syn match d2Boolean /\%(true\|false\)\%(\s*[\n#;[\]{}|`$'"\\]\)\@=/ contained 138 | 139 | " ******************************************************************************** 140 | " @d2Number 141 | " ******************************************************************************** 142 | " https://go.dev/ref/spec#Integer_literals 143 | " https://go.dev/ref/spec#Floating-point_literals 144 | " $VIMRUNTIME/syntax/go.vim 145 | 146 | syn match d2NumberDecimal /[+-]\?[0-9_]*\.\?[0-9_]\+\%([eEpP][+-]\?[0-9_]*\)\?\%(\s*[\n#;[\]{}|`$'"\\]\)\@=/ contained 147 | syn match d2NumberDecimal /[+-]\?[0-9_]\+\.\?[0-9_]*\%([eEpP][+-]\?[0-9_]*\)\?\%(\s*[\n#;[\]{}|`$'"\\]\)\@=/ contained 148 | 149 | syn match d2NumberOctal /[+-]\?0[oO]\?[0-7_]*\%([eEpP][+-]\?[0-9_]*\)\?\%(\s*[\n#;[\]{}|`$'"\\]\)\@=/ contained 150 | syn match d2NumberHex /[+-]\?0[xX][[:xdigit:]_]*\.\?[[:xdigit:]_]*\%([eEpP][+-]\?[0-9_]*\)\?\%(\s*[\n#;[\]{}|`$'"\\]\)\@=/ contained 151 | syn match d2NumberBinary /[+-]\?0[bB][01_]*\.\?[01_]*\%([eEpP][+-]\?[0-9_]*\)\?\%(\s*[\n#;[\]{}|`$'"\\]\)\@=/ contained 152 | 153 | syn cluster d2Number contains=d2NumberDecimal,d2NumberOctal,d2NumberHex,d2NumberBinary 154 | 155 | hi def link d2NumberDecimal d2Number 156 | hi def link d2NumberOctal d2Number 157 | hi def link d2NumberHex d2Number 158 | hi def link d2NumberBinary d2Number 159 | 160 | " ******************************************************************************** 161 | " d2Array 162 | " ******************************************************************************** 163 | 164 | syn region d2Array matchgroup=d2Delimiter start=/\%([;:]\%(\s*\\\n\)\?\s*\|\\\@d2 *d2* 76 | Context-sensitive mapping: 77 | - Visual mode (any file): Render selected text as ASCII (|:D2PreviewSelection|) 78 | - Normal mode (D2 files): Render entire buffer as ASCII (|:D2Preview|) 79 | 80 | rd2 *rd2* 81 | Replace selected D2 code with ASCII render (|:D2ReplaceSelection|). 82 | Available in visual mode for any file. Permanently replaces the selected 83 | text with the ASCII diagram output. 84 | 85 | yd2 *yd2* 86 | Copy ASCII preview content to clipboard and yank register (|:D2PreviewCopy|). 87 | Available in normal mode for any file. Requires an open preview window. 88 | 89 | ============================================================================== 90 | ASCII PREVIEW *d2-ascii-preview* 91 | 92 | The ASCII preview feature renders D2 diagrams as text in a vertical split pane 93 | within Vim. This provides a live preview of your diagrams without requiring 94 | external tools or graphics support. 95 | 96 | Requirements: D2 version 0.7.1 or higher is required for ASCII output. 97 | 98 | Use cases: 99 | - Quick previews during development 100 | - Working in terminal-only environments 101 | - Sharing diagrams in text-only contexts 102 | - Understanding diagram structure while editing 103 | 104 | The preview window automatically updates when you save your D2 file (if 105 | |g:d2_ascii_autorender| is enabled, which is the default). 106 | 107 | ASCII MODES *d2-ascii-modes* 108 | 109 | D2 supports two ASCII rendering modes: 110 | 111 | Extended Mode (default): 112 | Uses Unicode box-drawing characters for cleaner output. Produces diagrams 113 | with smooth lines and proper corners using characters like ┌─┐│└┘. 114 | 115 | Standard Mode: 116 | Uses basic ASCII characters for maximum compatibility. Produces diagrams 117 | using only standard ASCII characters like +---|. 118 | 119 | The mode is controlled by |g:d2_ascii_mode|. When set to "standard", the 120 | --ascii-mode=standard flag is passed to the d2 command. 121 | 122 | ============================================================================== 123 | SETTINGS *d2-settings* 124 | 125 | *g:d2_fmt_autosave* 126 | Enable or disable automatic formatting on save. 127 | Default: 1 (enabled) 128 | 129 | *g:d2_fmt_command* 130 | Customize the command used for formatting. 131 | Default: "d2 fmt" 132 | 133 | *g:d2_fmt_fail_silently* 134 | Control whether formatting errors are displayed. 135 | Default: 0 (show errors) 136 | 137 | *g:d2_validate_autosave* 138 | Enable or disable automatic validation on save. 139 | Default: 0 (disabled) 140 | 141 | *g:d2_validate_command* 142 | Customize the command used for validation. 143 | Default: "d2 validate" 144 | 145 | *g:d2_validate_fail_silently* 146 | Control whether validation errors are displayed. 147 | Default: 0 (show errors) 148 | 149 | *g:d2_list_type* 150 | Choose between quickfix and location list for displaying validation errors. 151 | Options: "quickfix" or "locationlist" 152 | Default: "quickfix" 153 | 154 | *g:d2_play_command* 155 | Customize the command used for playground. 156 | Default: "d2 play" 157 | 158 | *g:d2_play_theme* 159 | Set the theme ID for the playground. 160 | Default: 0 161 | 162 | *g:d2_play_sketch* 163 | Enable sketch mode for the playground (hand-drawn appearance). 164 | Default: 0 (disabled) 165 | 166 | *g:d2_ascii_command* 167 | Customize the command used for ASCII rendering. 168 | Default: "d2" 169 | 170 | *g:d2_ascii_preview_width* 171 | Set the width of the ASCII preview window in columns (vertical split). 172 | Default: half the screen width (&columns / 2) 173 | 174 | *g:d2_ascii_autorender* 175 | Enable or disable automatic ASCII rendering on save. 176 | Default: 1 (enabled) 177 | 178 | *g:d2_ascii_mode* 179 | Set the ASCII rendering mode. Options: "extended" or "standard". 180 | Extended uses Unicode box-drawing characters, standard uses basic ASCII. 181 | Default: "extended" 182 | 183 | *g:d2_block_string_syntaxes* 184 | 185 | This option declares a map from vim syntax names to the tags that |d2-vim| 186 | should enable each within block strings. 187 | 188 | Default: > 189 | let g:d2_block_string_syntaxes = { 190 | \ 'd2': ['d2'], 191 | \ 'markdown': ['md', 'markdown'], 192 | \ 'javascript': ['javascript', 'js'], 193 | \ 'html': ['html'], 194 | \ 'json': ['json'], 195 | \ 'c': ['c'], 196 | \ 'go': ['go'], 197 | \ 'sh': ['sh', 'ksh', 'bash'], 198 | \ 'css': ['css'], 199 | \ 'vim': ['vim'], 200 | \ } 201 | < 202 | Example: > 203 | x: |`js let x = 3 `| 204 | < 205 | To extend the default with another syntax, say TypeScript: > 206 | let g:d2_block_string_syntaxes = {'typescript': ['typescript', 'ts']} 207 | < 208 | Now the following block strings will have syntax highlighting: > 209 | x: |`typescript let x = 3 `| 210 | x: |`ts let x = 3 `| 211 | 212 | To extend the default with another tag on a syntax, say html: > 213 | let g:d2_block_string_syntaxes = {'html': ['htm']} 214 | < 215 | Now the following block string will have HTML syntax highlighting: > 216 | x: |`htm hello world `| 217 | 218 | The user value of |g:d2_block_string_syntaxes| is merged with the default so 219 | the following block strings continue to have syntax highlighting: > 220 | x: |`js let x = 3 `| 221 | x: |`html hello world `| 222 | < 223 | To disable block string syntaxes: > 224 | let g:d2_block_string_syntaxes = v:false 225 | < 226 | The value of |g:markdown_fenced_languages| is automatically synced with 227 | |g:d2_block_string_syntaxes|. See https://github.com/tpope/vim-markdown for 228 | documentation on |g:markdown_fenced_languages|. 229 | 230 | note: In markdown block strings, |d2-vim| disables indented code blocks 231 | highlighting as there's no way to make markdown.vim aware of the indent of 232 | the block string and so it ends up wrongly highlighting 4 space indent block 233 | strings as indented code blocks. 234 | 235 | Indented code blocks are rarely used anyway. 236 | You almost always want to use a fenced code block instead. 237 | See https://spec.commonmark.org/0.30/#indented-code-blocks 238 | See https://spec.commonmark.org/0.30/#fenced-code-blocks 239 | 240 | note: Line continuations are known to be buggy within fenced d2 code blocks 241 | of top level markdown files. 242 | 243 | ============================================================================== 244 | vim: ft=help tw=78 et ts=2 sw=0 sts=0 fo-=t norl 245 | -------------------------------------------------------------------------------- /autoload/d2/ascii.vim: -------------------------------------------------------------------------------- 1 | " Cached version check result 2 | let s:version_check_cache = {} 3 | 4 | " Check if d2 version supports ASCII output (with caching) 5 | function! s:get_d2_version_check() abort 6 | let l:cmd = get(g:, 'd2_ascii_command', 'd2') 7 | 8 | " Return cached result if available 9 | if has_key(s:version_check_cache, l:cmd) 10 | return s:version_check_cache[l:cmd] 11 | endif 12 | 13 | " Perform version check 14 | let l:result = system(l:cmd . ' version') 15 | 16 | if v:shell_error != 0 17 | let l:check_result = {'valid': 0, 'version': 'unknown', 'error': 'd2 command not found or failed'} 18 | let s:version_check_cache[l:cmd] = l:check_result 19 | return l:check_result 20 | endif 21 | 22 | " Extract version number (format: "d2 version v0.7.1") 23 | let l:version_match = matchstr(l:result, 'v\?\zs\d\+\.\d\+\.\d\+') 24 | if l:version_match == '' 25 | let l:check_result = {'valid': 0, 'version': 'unknown', 'error': 'could not parse version'} 26 | let s:version_check_cache[l:cmd] = l:check_result 27 | return l:check_result 28 | endif 29 | 30 | " Parse version components 31 | let l:parts = split(l:version_match, '\.') 32 | if len(l:parts) != 3 33 | let l:check_result = {'valid': 0, 'version': l:version_match, 'error': 'invalid version format'} 34 | let s:version_check_cache[l:cmd] = l:check_result 35 | return l:check_result 36 | endif 37 | 38 | let l:major = str2nr(l:parts[0]) 39 | let l:minor = str2nr(l:parts[1]) 40 | let l:patch = str2nr(l:parts[2]) 41 | 42 | " Check if version >= 0.7.1 43 | let l:valid = 0 44 | if l:major > 0 45 | let l:valid = 1 46 | elseif l:major == 0 && l:minor > 7 47 | let l:valid = 1 48 | elseif l:major == 0 && l:minor == 7 && l:patch >= 1 49 | let l:valid = 1 50 | endif 51 | 52 | let l:check_result = {'valid': l:valid, 'version': l:version_match, 'error': ''} 53 | let s:version_check_cache[l:cmd] = l:check_result 54 | return l:check_result 55 | endfunction 56 | 57 | " d2#ascii#Preview renders the current buffer as ASCII in a preview window 58 | function! d2#ascii#Preview() abort 59 | " let l:version_check = s:get_d2_version_check() 60 | " if !l:version_check.valid 61 | " echohl ErrorMsg 62 | " if l:version_check.version != 'unknown' 63 | " echo 'd2 ASCII preview requires version 0.7.1+. Current version: ' . l:version_check.version 64 | " else 65 | " echo 'd2 ASCII preview requires version 0.7.1+. ' . l:version_check.error 66 | " endif 67 | " echohl None 68 | " return 69 | " endif 70 | let l:tmpname = tempname() . '.d2' 71 | let l:output_file = tempname() . '.txt' 72 | 73 | silent execute 'write! ' . fnameescape(l:tmpname) 74 | 75 | let l:cmd = get(g:, 'd2_ascii_command', 'd2') 76 | 77 | " Add ascii-mode flag if not extended 78 | let l:ascii_mode = get(g:, 'd2_ascii_mode', 'extended') 79 | if l:ascii_mode == 'standard' 80 | let l:cmd .= ' --ascii-mode=standard' 81 | endif 82 | 83 | let l:cmd .= ' ' . shellescape(l:tmpname) . ' ' . shellescape(l:output_file) 84 | 85 | let l:result = system(l:cmd) 86 | let l:exit_code = v:shell_error 87 | 88 | call delete(l:tmpname) 89 | 90 | if l:exit_code != 0 91 | echohl ErrorMsg 92 | echo 'd2 ascii render failed: ' . substitute(l:result, '\n$', '', '') 93 | echohl None 94 | if filereadable(l:output_file) 95 | call delete(l:output_file) 96 | endif 97 | return 98 | endif 99 | 100 | if !filereadable(l:output_file) 101 | echohl ErrorMsg 102 | echo 'd2 ascii render failed: output file not created' 103 | echohl None 104 | return 105 | endif 106 | 107 | call s:show_ascii_preview(l:output_file) 108 | call delete(l:output_file) 109 | endfunction 110 | 111 | " d2#ascii#PreviewToggle toggles the ASCII preview window 112 | function! d2#ascii#PreviewToggle() abort 113 | if s:preview_window_exists() 114 | call s:close_preview_window() 115 | else 116 | call d2#ascii#Preview() 117 | endif 118 | endfunction 119 | 120 | " d2#ascii#PreviewUpdate updates the existing ASCII preview 121 | function! d2#ascii#PreviewUpdate() abort 122 | if s:preview_window_exists() 123 | let l:current_win = winnr() 124 | call d2#ascii#Preview() 125 | execute l:current_win . 'wincmd w' 126 | endif 127 | endfunction 128 | 129 | " Show ASCII content in a preview window 130 | function! s:show_ascii_preview(output_file) abort 131 | let l:preview_width = get(g:, 'd2_ascii_preview_width', 80) 132 | 133 | " Close existing preview if it exists 134 | call s:close_preview_window() 135 | 136 | " Create new vertical preview window 137 | execute 'vertical botright ' . l:preview_width . 'split D2_ASCII_PREVIEW' 138 | 139 | " Configure the preview buffer 140 | setlocal buftype=nofile 141 | setlocal bufhidden=wipe 142 | setlocal noswapfile 143 | setlocal nowrap 144 | setlocal filetype= 145 | setlocal foldcolumn=0 146 | setlocal nofoldenable 147 | setlocal nonumber 148 | setlocal norelativenumber 149 | setlocal nospell 150 | 151 | " Read the ASCII content 152 | execute 'read ' . fnameescape(a:output_file) 153 | 154 | " Remove the empty first line 155 | 1delete 156 | 157 | " Go back to original window 158 | wincmd p 159 | endfunction 160 | 161 | " Check if ASCII preview window exists 162 | function! s:preview_window_exists() abort 163 | return bufwinnr('D2_ASCII_PREVIEW') != -1 164 | endfunction 165 | 166 | " Close ASCII preview window 167 | function! s:close_preview_window() abort 168 | let l:preview_winnr = bufwinnr('D2_ASCII_PREVIEW') 169 | if l:preview_winnr != -1 170 | execute l:preview_winnr . 'wincmd w' 171 | close 172 | wincmd p 173 | endif 174 | endfunction 175 | 176 | " d2#ascii#ToggleAutoRender toggles automatic ASCII rendering on save 177 | function! d2#ascii#ToggleAutoRender() abort 178 | let g:d2_ascii_autorender = !get(g:, 'd2_ascii_autorender', 1) 179 | if g:d2_ascii_autorender 180 | echo 'Auto ASCII render enabled' 181 | else 182 | echo 'Auto ASCII render disabled' 183 | endif 184 | endfunction 185 | 186 | " d2#ascii#CopyPreview copies the ASCII preview window content to clipboard 187 | function! d2#ascii#CopyPreview() abort 188 | if !s:preview_window_exists() 189 | echohl ErrorMsg 190 | echo 'No ASCII preview window open' 191 | echohl None 192 | return 193 | endif 194 | 195 | let l:current_win = winnr() 196 | let l:preview_winnr = bufwinnr('D2_ASCII_PREVIEW') 197 | 198 | " Switch to preview window 199 | execute l:preview_winnr . 'wincmd w' 200 | 201 | " Get all lines from the preview buffer 202 | let l:lines = getline(1, '$') 203 | let l:content = join(l:lines, "\n") 204 | 205 | " Copy to clipboard and yank register 206 | let @+ = l:content 207 | let @* = l:content 208 | let @" = l:content 209 | 210 | " Switch back to original window 211 | execute l:current_win . 'wincmd w' 212 | 213 | echo 'ASCII preview copied to clipboard' 214 | endfunction 215 | 216 | " d2#ascii#ReplaceSelection replaces selected D2 code with ASCII render 217 | function! d2#ascii#ReplaceSelection() range abort 218 | " let l:version_check = s:get_d2_version_check() 219 | " if !l:version_check.valid 220 | " echohl ErrorMsg 221 | " if l:version_check.version != 'unknown' 222 | " echo 'd2 ASCII replace requires version 0.7.1+. Current version: ' . l:version_check.version 223 | " else 224 | " echo 'd2 ASCII replace requires version 0.7.1+. ' . l:version_check.error 225 | " endif 226 | " echohl None 227 | " return 228 | " endif 229 | let l:tmpname = tempname() . '.d2' 230 | let l:output_file = tempname() . '.txt' 231 | 232 | " Get selected text 233 | let l:lines = getline(a:firstline, a:lastline) 234 | 235 | " Detect comment prefix from the first line 236 | let l:comment_prefix = '' 237 | if len(l:lines) > 0 238 | let l:first_line = l:lines[0] 239 | " Match common comment patterns: //, #, --, *,