Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Tag generator for Rmarkdown files
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
help_text = """
Extracts tags from rmd files. Useful for the Tagbar plugin.
Usage:
Install Tagbar (http://majutsushi.github.io/tagbar/). Then, put this file
anywhere and add the following to your .vimrc:
let g:tagbar_type_rmd = {
\ 'ctagstype':'rmd'
\ , 'kinds':['h:header', 'c:chunk', 'f:function', 'v:variable']
\ , 'sro':'&&&'
\ , 'kind2scope':{'h':'header', 'c':'chunk', 'f':'function', 'v':'variable'}
\ , 'sort':0
\ , 'ctagsbin':'/path/to/rmdtags.py'
\ . 'ctagsargs':''
\ }
"""
import sys
import re
if len(sys.argv) < 2:
print(help_text)
exit()
filename = sys.argv[1]
# filename = "test.Rmd"
print(filename)
re_header = re.compile(r"^(#+)([^{]+)(\{[^}]+\})?$")
re_chunk = re.compile(r"^```\{r ([^,]+)(,[^}]*)?\}$")
re_function = re.compile(r"^[\t ]*([^ ]+) *<- *function.*$")
re_variable1 = re.compile(r"^[\t ]*([^ ]+) *<-.*$")
re_variable2 = re.compile(r"^.*-> *(.+)$")
file_content = []
try:
with open(filename, "r") as vim_buffer:
file_content = vim_buffer.readlines()
except:
exit()
headers = []
depth = [0]
inChunk = False
curChunk = None
for lnum, line in enumerate(file_content):
newline = []
tag = ""
signature = ""
options = ""
# Headers
if re_header.match(line) and not inChunk:
level = len(re_header.match(line).group(1))
tag = re_header.match(line).group(2).strip()
newline.append(tag)
newline.append(filename)
newline.append('/^' + line.rstrip("\n") + '$/;"')
newline.append("h")
newline.append("line:" + str(lnum+1))
# header
while (level <= depth[len(depth) -1]):
headers.pop()
depth.pop()
if len(headers) > 0:
options = "header:" + "&&&".join(headers)
headers.append(tag)
depth.append(level)
# signature
if re_header.match(line).group(3):
signature = "(" + re_header.match(line).group(3).strip("{}") + ")"
options = options + "\tsignature:" + signature
# Print
newline.append(options)
print("\t".join(newline))
# Chunks
if re_chunk.match(line):
inChunk = True
curChunk = re_chunk.match(line).group(1).strip()
newline.append(re_chunk.match(line).group(1).strip())
newline.append(filename)
newline.append('/^' + line.rstrip("\n") + '$/;"')
newline.append("c")
newline.append("line:" + str(lnum+1))
if len(headers) > 0:
options = "header:" + "&&&".join(headers)
if re_chunk.match(line).group(2):
signature = "(" + re_chunk.match(line).group(2).lstrip(", ") + ")"
options = options + "\tsignature:" + signature
# Print
newline.append(options)
print("\t".join(newline))
if line == "```\n":
inChunk = False
curChunk = None
# Functions
if re_function.match(line) and inChunk:
newline.append(re_function.match(line).group(1).strip())
newline.append(filename)
newline.append('/^' + line.rstrip("\n") + '$/;"')
newline.append("f")
newline.append("line:" + str(lnum+1))
print("\t".join(newline))
elif (re_variable1.match(line) or re_variable2.match(line)) and inChunk:
tag = re_variable1.match(line) or re_variable2.match(line)
tag = tag.group(1).strip()
newline.append(tag)
newline.append(filename)
newline.append('/^' + line.rstrip("\n") + '$/;"')
newline.append("v")
newline.append("line:" + str(lnum+1))
print("\t".join(newline))
@kguidonimartins

This comment has been minimized.

Copy link

@kguidonimartins kguidonimartins commented Nov 17, 2020

Hi Maxime, thanks for this tag generator. I found this gist through your blog. I've been using your script for the past 6 months, maybe. I would like to expand the list of tags and include inline chunks that are located in the main text as:

Our data have `r nrow(df)` observations...

I think the variable for finding inline chunks could be (but I may be wrong):

re_inlinechunk = re.compile(r"^`r ([^,]+)(,[^}]*)?`$")

But I have no idea how to allocate it in your code. Any idea? Thanks again!

@MaximeWack

This comment has been minimized.

Copy link
Owner Author

@MaximeWack MaximeWack commented Nov 18, 2020

Hi Karlo,

I'm not using nvim anymore (hence not relying on this script anymore), so I won't be able to test it through and through.

You should remove the beginning of line and end of line anchors in your regexp, also there is no need to check for chunk options, so this should suffice:

re_inlinechunk = re.compile(r"`r ([^`])+`")

As for implementing the tag, you should insert this block (or something close to that, I haven't tested it) probably just before the logic for matching headers (# Headers).
You can go further and add the logic to place the inline chunks inside the headers, in the same fashion as with the block chunks and other headers.

if re_inlinechunk.match(line) and not inChunk:
        newline.append(re_inlinechunk.match(line).group(1).strip())
        newline.append(filename)
        newline.append('/^' + line.rstrip("\n") + '$/;"')
        newline.append("c")
        newline.append("line:" + str(lnum+1))
        print("\t".join(newline))

This should add the inline chunks as regular chunks in tagbar.
If you want a different behavior, you have to change the "c" kind to a new letter (that is not already used by another kind of tag), and declare that tag kind in kinds and kind2scope

@kguidonimartins

This comment has been minimized.

Copy link

@kguidonimartins kguidonimartins commented Nov 19, 2020

Hi Maxime,

I will try this and, hopefully, I will do my best to make it work.

Thank you very much for your time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment