commit 85de24c998192da98ced4daec9e44f28dedc55cf
parent 49f011e20697af65bcdb8dae0f1bf59718301275
Author: Alex Balgavy <alex@balgavy.eu>
Date: Sun, 27 Jun 2021 13:33:38 +0200
Add ability to tangle to different files, and based on language
You can switch the file you're tangling to throughout the document. You
can specify the tangle file for a particular language by adding the
language as a parameter, i.e. :Tangle(python). The language parameter
has to match the language in the header of the block. If a file isn't
specified for a language, the general :Tangle file is used.
Diffstat:
2 files changed, 130 insertions(+), 38 deletions(-)
diff --git a/after/ftplugin/markdown.vim b/after/ftplugin/markdown.vim
@@ -1,3 +1,7 @@
+if exists('g:loaded_literate_markdown')
+ finish
+endif
+
command -buffer -bar Tangle call literate_markdown#Tangle()
command -buffer -bar ExecPrevBlock call literate_markdown#ExecPreviousBlock()
nnoremap <buffer> <Plug>LitMdExecPrevBlock :<c-u>ExecPrevBlock<CR>
diff --git a/autoload/literate_markdown.vim b/autoload/literate_markdown.vim
@@ -7,31 +7,26 @@ let g:loaded_literate_markdown_autoload = 1
let s:codeblock_start = '^ *```[a-z]\+'
let s:codeblock_end = '^ *```$'
-let s:tangle_directive = '<!-- *:Tangle '
+let s:tangle_directive = '<!-- *:Tangle'
let s:result_comment_start = '<!--\nRESULT:'
let s:result_comment_end = '^-->'
+let s:ALL_INTERP = ''
-function! s:GetFilename()
- " TODO: add optional parameter to tangle only specific language
- " maybe format like: <!-- :Tangle(lang) /path/to/file -->
-
- let ln = search(s:tangle_directive, 'n')
- if ln ==# 0
- echoerr 'No :Tangle directive found'
- return ''
- else
- let fname = getline(ln)->matchstr(':Tangle \zs.*\ze -->')
- if fname ==# ''
- echoerr 'No filename set.'
- else
- if fname[0] !=# '/'
- let fname = expand("%:p:h") . '/' . fname
- endif
- return fnameescape(fname)
- endif
+" returns [interpreter, filename]
+function! s:ParseTangleDirective(ln)
+ let theline = getline(a:ln)->matchlist(':Tangle\(([a-zA-Z0-9]*)\)* \(.*\) -->')[0:2]
+ if empty(theline)
+ echoerr 'No filename in Tangle directive on line ' .. a:ln
+ return []
+ endif
+ let [_, interp, fname] = theline
+ if fname[0] !=# '/'
+ let fname = expand("%:p:h") . '/' . fname
endif
+ return [s:Lang2Interpreter(interp[1:-2]), fnameescape(fname)]
endfunction
+" returns end line of a code block
function! s:GetBlockEnd(start)
" Save the cursor
let rowsave = line('.')
@@ -46,6 +41,7 @@ function! s:GetBlockEnd(start)
return endblock
endfunction
+" returns [contents of a code block]
function! s:GetBlockContents(start, end)
" i.e. if there's no end
if a:end ==# 0
@@ -56,47 +52,123 @@ function! s:GetBlockContents(start, end)
return retval
endfunction
+" returns all code in the current buffer in the form
+" {'interpreter': {'file1': [line1, line2], 'file2': [line1, line2]}...}
function! s:GetAllCode()
- let codelines = []
- let endline = line("$")
+ let codelines = {}
+
+ " The current files set for various interpreters
+ let curfiles = {}
+
+ " The interpreter specified in the most recent Tangle directive
+ let last_set_interp = s:ALL_INTERP
let curline = 1
+ let endline = line("$")
+
+ " Loop through lines
while curline <=# endline
- let block_pos_on_this_line = match(getline(curline), s:codeblock_start)
+ " If this line has a Tangle directive
+ if match(getline(curline), s:tangle_directive) >=# 0
+
+ " Try to parse the directive
+ let parsedline = s:ParseTangleDirective(curline)
+ if empty(parsedline)
+ return {}
+ endif
+ let [last_set_interp, curfile] = parsedline
- if block_pos_on_this_line >=# 0
- let block_contents = s:GetBlockContents(curline, s:GetBlockEnd(curline))
- if len(block_contents) ==# 0
- echoerr 'No end of block starting on line '.curline
- return []
+ " Change the current file for the interpreter
+ let curfiles[last_set_interp] = curfile
+
+ " If the interpreter has already been specified
+ if has_key(codelines, last_set_interp)
+ " If the interpreter does not yet have any lines for this file
+ if !has_key(codelines[last_set_interp], curfile)
+ " Add it
+ let codelines[last_set_interp][curfile] = []
+ endif
+ " If the interpreter already has lines for the file, don't do anything
+ " If the interpreter itself hasn't been specified yet
else
+ " Add it
+ let codelines[last_set_interp] = {curfile: []}
+ endif
+ " Go to next line
+ let curline += 1
+ else
+ " Find a block on this line
+ let block_pos_on_this_line = match(getline(curline), s:codeblock_start)
+
+ " If there's a block, process it
+ if block_pos_on_this_line >=# 0
+ " Get the contents of this block
+ let block_contents = s:GetBlockContents(curline, s:GetBlockEnd(curline))
+
+ if len(block_contents) ==# 0
+ echoerr 'No end of block starting on line '.curline
+ return {}
+ endif
+
+ " Find out the amount of leading indentation (using the first line)
let nleadingspaces = matchend(block_contents[0], '^ \+')
if nleadingspaces ==# -1
let nleadingspaces = 0
endif
- if len(codelines) !=# 0
- call add(codelines, '')
+
+ " Get the interpreter for this block
+ let block_interp = s:GetBlockInterpreter(curline)
+ if !empty(block_interp)
+ " Allow overriding all interpreters to a 'general' file:
+ " If the last Tangle directive didn't have an interpreter, direct
+ " all blocks to that file
+ if last_set_interp ==# ""
+ " Get the current file for 'all interpreters'
+ let curfile = curfiles[last_set_interp]
+ let curinterp = s:ALL_INTERP
+ " If the last Tangle directive specified an interpreter
+ else
+ " If the interpreter was specified in a Tangle directive, use its
+ " current file
+ if has_key(codelines, block_interp)
+ let curfile = curfiles[block_interp]
+ let curinterp = block_interp
+ " Otherwise, use the 'general' file if specified
+ elseif has_key(codelines, s:ALL_INTERP)
+ let curfile = curfiles[s:ALL_INTERP]
+ let curinterp = s:ALL_INTERP
+ endif
+ endif
+
+ " Add the lines to the current file to the current interpreter,
+ " stripping leading indentation and appending a newline
+ if exists('curinterp')
+ call extend(codelines[curinterp][curfile], (map(block_contents, 'v:val['.nleadingspaces.':]')+['']))
+ endif
endif
- call extend(codelines, map(block_contents, 'v:val['.nleadingspaces.':]'))
+ " Skip to after the block
let curline += len(block_contents)+2
+ " Otherwise, go to the next line
+ else
+ let curline += 1
endif
- else
- let curline += 1
endif
endwhile
return codelines
endfunction
+" Write [lines] to fname and open it in a split
function! s:SaveLines(lines, fname)
if writefile(a:lines, a:fname) ==# 0
- exe 'drop '.a:fname
+ exe 'split '.a:fname
else
echoerr "Could not write to file ".a:fname
endif
endfunction
+" Returns the interpreter name for a programming language
function! s:Lang2Interpreter(lang)
let lang = a:lang
if exists('g:literate_markdown_interpreters')
@@ -129,6 +201,7 @@ function! s:Lang2Interpreter(lang)
return ''
endfunction
+" Gets the interpreter name for a code block
function! s:GetBlockInterpreter(blockstart)
" A markdown block beginning looks like this: ```lang
let lang = getline(a:blockstart)[3:]
@@ -204,13 +277,28 @@ function! literate_markdown#ExecPreviousBlock()
endfunction
function! literate_markdown#Tangle()
- let fname = s:GetFilename()
- if fname ==# ''
- return
- endif
+ " Get all of the code blocks in the file
let lines = s:GetAllCode()
+
+ " If there's any, tangle it
if len(lines) ># 0
- call s:SaveLines(lines, fname)
+ " Merge lines from all interpreters into the files
+ let all_interps_combined = {}
+ for fname_and_lines in lines->values()
+ for [fname, flines] in fname_and_lines->items()
+ if all_interps_combined->has_key(fname)
+ call extend(all_interps_combined[fname], flines)
+ else
+ let all_interps_combined[fname] = flines
+ endif
+ endfor
+ endfor
+
+ " Loop through the filenames and corresponding code
+ for [fname, flines] in items(all_interps_combined)
+ " Write the code to the respective file
+ call s:SaveLines(flines, fname)
+ endfor
endif
endfunction