From d3260507e776312d1c83df5ef6784353523e931c Mon Sep 17 00:00:00 2001 From: Roberto Castagnola Date: Sat, 12 Nov 2022 17:15:48 +0100 Subject: [PATCH] Add markdown parser --- autoload/lsp/lsp.vim | 26 ++ autoload/lsp/lspserver.vim | 9 +- autoload/lsp/markdown.vim | 692 +++++++++++++++++++++++++++++++++++++ ftplugin/lspgfm.vim | 23 ++ syntax/lspgfm.vim | 23 ++ 5 files changed, 766 insertions(+), 7 deletions(-) create mode 100644 autoload/lsp/markdown.vim create mode 100644 ftplugin/lspgfm.vim create mode 100644 syntax/lspgfm.vim diff --git a/autoload/lsp/lsp.vim b/autoload/lsp/lsp.vim index 22b1b7b..cfe7ec7 100644 --- a/autoload/lsp/lsp.vim +++ b/autoload/lsp/lsp.vim @@ -293,6 +293,11 @@ def AddBufLocalAutocmds(lspserver: dict, bnr: number): void endif endif + acmds->add({bufnr: bnr, + event: 'CompleteChanged', + group: 'LSPBufferAutocmds', + cmd: 'LspSetFileType()'}) + # Execute LSP server initiated text edits after completion acmds->add({bufnr: bnr, event: 'CompleteDone', @@ -669,6 +674,27 @@ def LspResolve() endif enddef +# If the completion popup documentation window displays 'markdown' content, +# then set the 'filetype' to 'lspgfm'. +def LspSetFileType() + var item = v:event.completed_item + if !item->has_key('user_data') || empty(item.user_data) + return + endif + + var cItem = item.user_data + if !cItem->has_key('documentation') || cItem->type() != v:t_dict + || cItem.documentation.kind != 'markdown' + return + endif + + var id = popup_findinfo() + if id > 0 + var bnum = id->winbufnr() + setbufvar(bnum, '&ft', 'lspgfm') + endif +enddef + # omni complete handler def g:LspOmniFunc(findstart: number, base: string): any var lspserver: dict = CurbufGetServerChecked() diff --git a/autoload/lsp/lspserver.vim b/autoload/lsp/lspserver.vim index 88bda4b..a22d2f3 100644 --- a/autoload/lsp/lspserver.vim +++ b/autoload/lsp/lspserver.vim @@ -974,7 +974,7 @@ def CompletionResolveReply(lspserver: dict, cItem: dict) infoKind = 'text' elseif cItem.documentation.kind == 'markdown' infoText->extend(cItem.documentation.value->split("\n")) - infoKind = 'markdown' + infoKind = 'lspgfm' else util.ErrMsg($'Error: Unsupported documentation type ({cItem.documentation.kind})') return @@ -1003,13 +1003,8 @@ def CompletionResolveReply(lspserver: dict, cItem: dict) var id = popup_findinfo() if id > 0 var bufnr = id->winbufnr() - infoKind->setbufvar(bufnr, '&ft') - if infoKind == 'markdown' - 3->setwinvar(id, '&conceallevel') - else - 0->setwinvar(id, '&conceallevel') - endif id->popup_settext(infoText) + infoKind->setbufvar(bufnr, '&ft') id->popup_show() endif enddef diff --git a/autoload/lsp/markdown.vim b/autoload/lsp/markdown.vim new file mode 100644 index 0000000..47a70f4 --- /dev/null +++ b/autoload/lsp/markdown.vim @@ -0,0 +1,692 @@ +vim9script + +# Markdown parser +# Refer to https://github.github.com/gfm/ +# for the GitHub Flavored Markdown specification. + +# TODO: different highlight for different heading level +# TODO: links +# TODO: pretty table + + +# Container blocks +var block_quote = '^ \{,3\}\zs> \=' +var list_marker = '[-+*]\|[0-9]\{1,9}[.)]' +var list_item = $'^\%({list_marker}\)\ze\s*$\|^ \{{,3}}\zs\%({list_marker}\) \{{1,4}}\ze\S\|^ \{{,3}}\zs\%({list_marker}\) \{{5}}\ze\s*\S' +# pattern to match list items +export var list_pattern = $'^ *\%({list_marker}\) *' + + +# Leaf blocks +var blank_line = '^\s*$' +var thematic_break = '^ \{,3\}\([-_*]\)\%(\s*\1\)\{2,\}\s*$' +var code_fence = '^ \{,3\}\(`\{3,\}\|\~\{3,\}\)\s*\(\S*\)' +var code_indent = '^ \{4\}\zs\s*\S.*' +var paragraph = '^\s*\zs\S.\{-}\ze\s*$' + +var atx_heading = '^ \{,3}\zs\(#\{1,6}\) \(.\{-}\)\ze\%( #\{1,}\s*\)\=$' +var setext_heading = '^ \{,3}\zs\%(=\{1,}\|-\{1,}\)\ze *$' +var setext_heading_level = {"=": 1, "-": 2} + +var table_delimiter = '^|\=\zs *:\=-\{1,}:\= *\%(| *:\=-\{1,}:\= *\)*\ze|\=$' + +var punctuation = "[!\"#$%&'()*+,-./:;<=>?@[\\\\\\\]^_`{|}~]" + +# Setting text properties +highlight LspBold term=bold cterm=bold gui=bold +highlight LspItalic term=italic cterm=italic gui=italic +highlight LspStrikeThrough term=strikethrough cterm=strikethrough gui=strikethrough +prop_type_add('LspMarkdownBold', {highlight: 'LspBold'}) +prop_type_add('LspMarkdownItalic', {highlight: 'LspItalic'}) +prop_type_add('LspMarkdownStrikeThrough', {highlight: 'LspStrikeThrough'}) +prop_type_add('LspMarkdownHeading', {highlight: 'Function'}) +prop_type_add('LspMarkdownCode', {highlight: 'PreProc'}) +prop_type_add('LspMarkdownCodeBlock', {highlight: 'PreProc'}) +prop_type_add('LspMarkdownListMarker', {highlight: 'Special'}) +prop_type_add('LspMarkdownTableHeader', {highlight: 'Label'}) +prop_type_add('LspMarkdownTableMarker', {highlight: 'Special'}) + + +def GetMarkerProp(marker: string, col: number, ...opt: list): dict + if marker == 'list_item' + return { + type: 'LspMarkdownListMarker', + col: col, + length: opt[0] + } + elseif marker == 'code_block' + return { + type: 'LspMarkdownCodeBlock', + col: col, + end_lnum: opt[0], + end_col: opt[1] + } + elseif marker == 'heading' + return { + type: 'LspMarkdownHeading', + col: col, + length: opt[0] + } + elseif marker == 'table_header' + return { + type: 'LspMarkdownTableHeader', + col: col, + length: opt[0] + } + elseif marker == 'table_sep' + return { + type: 'LspMarkdownTableMarker', + col: col, + length: opt[0] + } + elseif marker == 'code_span' + return { + type: 'LspMarkdownCode', + col: col, + length: opt[0] + } + elseif marker == 'emphasis' + return { + type: 'LspMarkdownItalic', + col: col, + length: opt[0] + } + elseif marker == 'strong' + return { + type: 'LspMarkdownBold', + col: col, + length: opt[0] + } + elseif marker == 'strikethrough' + return { + type: 'LspMarkdownStrikeThrough', + col: col, + length: opt[0] + } + endif + return {} +enddef + +def GetCodeSpans(text: string): list> + var code_spans = [] + var pos = 0 + while pos < text->len() + var backtick = text->matchstrpos('\\*`', pos) + if backtick[1] < 0 + break + endif + if backtick[0]->len() % 2 == 0 + # escaped backtick + pos = backtick[2] + continue + endif + pos = backtick[2] - 1 + var code_span = text->matchstrpos('^\(`\+\)`\@!.\{-}`\@1matchstrpos('^\(`\+\)\%(\zs \+\ze\|\( \=\)\zs.\{-}\S.\{-}\ze\2\)`\@1add({ + marker: '`', + start: [code_span[1], code_text[1]], + end: [code_text[2], code_span[2]] + }) + pos = code_span[2] + endwhile + return code_spans +enddef + +def Unescape(text: string, block_marker: string = ""): string + if block_marker == '`' + return text + endif + return text->substitute($'\\\({punctuation}\)', '\1', 'g') +enddef + +def GetNextInlineDelimiter(text: string, start_pos: number, end_pos: number): dict + var pos = start_pos + while pos < text->len() + # search the first delimiter char + var delimiter = text->matchstrpos('\\*[_*~]', pos) + if delimiter[1] < 0 || delimiter[1] > end_pos + return {} + endif + if delimiter[0]->len() % 2 == 0 + # escaped delimiter char + pos = delimiter[2] + continue + endif + pos = delimiter[2] - 1 + var delimiter_run = text->matchstrpos( + $'{delimiter[0][-1]->substitute("\\([*~]\\)", "\\\\\\1", "g")}\+', + pos) + if delimiter_run[0][0] == '~' && delimiter_run[0]->len() > 2 + pos = delimiter_run[2] + continue + endif + var add_char = '' + if pos > 0 + pos -= 1 + add_char = '.' + endif + var delim_regex = delimiter_run[0]->substitute('\([*~]\)', '\\\1', 'g') + var is_left = text->match($'^{add_char}{delim_regex}\%(\s\|$\|{punctuation}\)\@!\|^{add_char}\%(\s\|^\|{punctuation}\)\@1<={delim_regex}{punctuation}', pos) >= 0 + var is_right = text->match($'^{add_char}\%(\s\|^\|{punctuation}\)\@1= 0 + if !is_left && ! is_right + pos = delimiter_run[2] + continue + endif + if delimiter_run[0][0] == '_' + && text->match($'^\w{delimiter_run[0]}\w', pos) >= 0 + # intraword emphasis is disallowed + pos = delimiter_run[2] + continue + endif + return { + marker: delimiter_run[0], + start: [delimiter_run[1], delimiter_run[2]], + left: is_left, + right: is_right + } + endwhile + return {} +enddef + +def GetNextInlineBlock(text: string, blocks: list, rel_pos: number): dict + var result = { + text: "", + props: [] + } + var cur = blocks->remove(0) + var pos = cur.start[1] + while blocks->len() > 0 && cur.end[0] >= blocks[0].start[0] + result.text ..= Unescape(text[pos : blocks[0].start[0] - 1], cur.marker[0]) + # get nested block + var part = GetNextInlineBlock(text, blocks, rel_pos + result.text->len()) + result.text ..= part.text + result.props += part.props + pos = part.end_pos + endwhile + result.text ..= Unescape(text[pos : cur.end[0] - 1], cur.marker[0]) + # add props for current inline block + var prop_type = { + '`': 'code_span', + '_': 'emphasis', + '__': 'strong', + '*': 'emphasis', + '**': 'strong', + '~': 'strikethrough', + '~~': 'strikethrough' + } + result.props->insert(GetMarkerProp(prop_type[cur.marker], + rel_pos + 1, + result.text->len())) + result->extend({'end_pos': cur.end[1]}) + return result +enddef + +def ParseInlines(text: string, rel_pos: number = 0): dict + var formatted = { + text: "", + props: [] + } + var code_spans = GetCodeSpans(text) + + var pos = 0 + var seq = [] + # search all emphasis + while pos < text->len() + var code_pos: list + if code_spans->len() > 0 + code_pos = [code_spans[0].start[0], code_spans[0].end[1]] + if pos >= code_pos[0] + pos = code_pos[1] + seq->add(code_spans->remove(0)) + continue + endif + else + code_pos = [text->len(), text->len()] + endif + var delimiter = GetNextInlineDelimiter(text, pos, code_pos[0]) + if delimiter->empty() + pos = code_pos[1] + continue + endif + if delimiter.right + var idx = seq->len() - 1 + while idx >= 0 + if delimiter.marker[0] != seq[idx].marker[0] + || seq[idx]->has_key('end') + idx -= 1 + continue + endif + if delimiter.left || seq[idx].right + # check the sum rule + if (delimiter.marker->len() + seq[idx].marker->len()) % 3 == 0 + && (delimiter.marker->len() % 3 > 0 + || seq[idx].marker->len() % 3 > 0) + # not valid condition + idx -= 1 + continue + endif + endif + var marker_len = min([delimiter.marker->len(), + seq[idx].marker->len(), 2]) + if seq[idx].marker->len() > marker_len + var new_delim = { + marker: delimiter.marker[0]->repeat(marker_len), + start: [seq[idx].start[1] - marker_len, seq[idx].start[1]], + left: v:true, + right: v:false + } + seq[idx].marker = seq[idx].marker[: -1 - marker_len] + seq[idx].start[1] -= marker_len + seq[idx].right = v:false + idx += 1 + seq->insert(new_delim, idx) + endif + seq[idx]->extend({ + end: [delimiter.start[0], + delimiter.start[0] + marker_len]}) + # close all overlapped emphasis spans not closed + for i in range(seq->len() - 1, idx + 1, -1) + if !seq[i]->has_key('end') + seq->remove(i) + endif + endfor + if delimiter.marker->len() > marker_len + delimiter.start[0] += marker_len + else + delimiter.left = v:false + break + endif + idx -= 1 + endwhile + endif + if delimiter.left + seq->add(delimiter) + endif + pos = delimiter.start[1] + endwhile + while code_spans->len() > 0 + seq->add(code_spans->remove(0)) + endwhile + # remove all not closed delimiters + for i in range(seq->len() - 1, 0, -1) + if !seq[i]->has_key('end') + seq->remove(i) + endif + endfor + + # compose final text + pos = 0 + while seq->len() > 0 + if pos < seq[0].start[0] + formatted.text ..= Unescape(text[pos : seq[0].start[0] - 1]) + pos = seq[0].start[0] + endif + var inline = GetNextInlineBlock(text, seq, + rel_pos + formatted.text->len()) + formatted.text ..= inline.text + formatted.props += inline.props + pos = inline.end_pos + endwhile + if pos < text->len() + formatted.text ..= Unescape(text[pos : -1]) + endif + return formatted +enddef + +# new open container block +def CreateContainerBlock(match: list, start_lnum: number): dict + if match[0][0] == '>' + return { + type: "quote_block", + lnum: start_lnum, + indent: 0 + } + else + return { + type: "list_item", + lnum: start_lnum, + marker: $' {match[0]->matchstr("\\S\\+")} ', + indent: match[2] + } + endif +enddef + +# new open leaf block +def CreateLeafBlock(block_type: string, line: string, ...opt: list): dict + if block_type == "fenced_code" + var token = line->matchlist(code_fence) + return { + type: block_type, + fence: token[1], + language: token[2], + text: [] + } + elseif block_type == "indented_code" + return { + type: block_type, + text: [line->matchstr(code_indent)] + } + elseif block_type == "paragraph" + return { + type: block_type, + text: [line->matchstr(paragraph)] + } + elseif block_type == "heading" + return { + type: block_type, + level: opt[0], + text: line + } + elseif block_type == "table" + return { + type: block_type, + header: line, + delimiter: opt[0], + text: [] + } + endif + return {} +enddef + +def NeedBlankLine(prev: string, cur: string): bool + if prev == "hr" || cur == "hr" + return v:false + elseif prev == "heading" || cur == "heading" + return v:true + elseif prev == "paragraph" && cur == "paragraph" + return v:true + elseif prev != cur + return v:true + endif + return v:false +enddef + +var last_block: string = "" + +def CloseBlocks(document: dict>, blocks: list>, start: number = 0): void + if start >= blocks->len() + return + endif + var line: dict = { + text: "", + props: [] + } + if !document.content->empty() && NeedBlankLine(last_block, blocks[0].type) + document.content->add({text: "", props: []}) + endif + last_block = blocks[0].type + + for i in range(start) + if blocks[i]->has_key("marker") + if blocks[i].marker =~ '\S' + line.props->add(GetMarkerProp('list_item', + line.text->len() + 1, + blocks[i].marker->len())) + line.text ..= blocks[i].marker + blocks[i].marker = ' '->repeat(blocks[i].marker->len()) + else + line.text ..= blocks[i].marker + endif + endif + endfor + for block in blocks->remove(start, -1) + if block.type =~ 'quote_block\|list_item' + if block->has_key("marker") + if block.marker =~ '\S' + line.props->add(GetMarkerProp('list_item', + line.text->len() + 1, + block.marker->len())) + line.text ..= block.marker + block.marker = ' '->repeat(block.marker->len()) + else + line.text ..= block.marker + endif + endif + else + # leaf block + if block.type =~ '_code' + if block.type == "indented_code" + while !block.text->empty() && block.text[0] !~ '\S' + block.text->remove(0) + endwhile + while !block.text->empty() && block.text[-1] !~ '\S' + block.text->remove(-1) + endwhile + endif + if !block.text->empty() + var indent = ' '->repeat(line.text->len()) + var max_len = mapnew(block.text, (_, l) => l->len())->max() + var text = block.text->remove(0) + line.text ..= text + document.content->add(line) + var startline = document.content->len() + for l in block.text + document.content->add({text: indent .. l}) + endfor + if block->has_key('language') + && !globpath(&rtp, $'syntax/{block.language}.vim')->empty() + document.syntax->add({lang: block.language, + start: $'\%{startline}l\%{indent->len() + 1}c', + end: $'\%{document.content->len()}l$'}) + else + line.props->add(GetMarkerProp('code_block', + indent->len() + 1, + document.content->len(), + indent->len() + max_len + 1)) + endif + endif + elseif block.type == "heading" + line.props->add(GetMarkerProp('heading', + line.text->len() + 1, + block.text->len(), + block.level)) + var format = ParseInlines(block.text, line.text->len()) + line.text ..= format.text + line.props += line.props + document.content->add(line) + elseif block.type == "table" + var indent = line.text + var head = block.header->split('\\\@1remove(0) + var format = ParseInlines(col1, line.text->len()) + line.props->add(GetMarkerProp('table_header', + line.text->len() + 1, + format.text->len())) + line.text ..= format.text + line.props += format.props + for colx in head + format = ParseInlines(colx, line.text->len() + 1) + line.props->add(GetMarkerProp('table_sep', line.text->len() + 1, 1)) + line.props->add(GetMarkerProp('table_header', + line.text->len() + 2, + format.text->len())) + line.text ..= "|" .. format.text + line.props += format.props + endfor + document.content->add(line) + var data = { + text: indent .. block.delimiter, + props: [GetMarkerProp('table_sep', + indent->len() + 1, + block.delimiter->len())] + } + document.content->add(data) + for row in block.text + data = { + text: indent, + props: [] + } + var cell = row->split('\\\@1remove(0) + format = ParseInlines(col1, data.text->len()) + data.text ..= format.text + data.props += format.props + for colx in cell + format = ParseInlines(colx, data.text->len() + 1) + data.props->add(GetMarkerProp('table_sep', + data.text->len() + 1, + 1)) + data.text ..= "|" .. format.text + data.props += format.props + endfor + document.content->add(data) + endfor + elseif block.type == "paragraph" + var format = ParseInlines(block.text->join(" "), line.text->len()) + line.text ..= format.text + line.props += format.props + document.content->add(line) + endif + endif + endfor +enddef + +export def ParseMarkdown(data: list): dict> + var document: dict> = {content: [], syntax: []} + var open_blocks: list> = [] + + for l in data + var line: string = l + var cur = 0 + + # for each open block check if current line continue it + while cur < open_blocks->len() + if open_blocks[cur].type == "quote_block" + var marker = line->matchstrpos(block_quote) + if marker[1] == -1 + break + endif + line = line[marker[2] :] + elseif open_blocks[cur].type == "list_item" + var marker = line->matchstrpos($'^ \{{{open_blocks[cur].indent}}}') + if marker[1] == -1 + break + endif + line = line[marker[2] :] + elseif open_blocks[cur].type == "fenced_code" + if line =~ $'^ \{{,3}}{open_blocks[cur].fence}{open_blocks[cur].fence[0]}* *$' + CloseBlocks(document, open_blocks, cur) + else + open_blocks[cur].text->add(line) + endif + cur = -1 + break + elseif open_blocks[cur].type == "indented_code" + var marker = line->matchstrpos(code_indent) + if marker[1] >= 0 + open_blocks[cur].text->add(marker[0]) + cur = -1 + endif + break + elseif open_blocks[cur].type == "paragraph" + if line =~ setext_heading + var marker = line->matchstrpos(setext_heading) + open_blocks->add(CreateLeafBlock( + "heading", + open_blocks->remove(cur).text->join(" "), + setext_heading_level[marker[0]])) + CloseBlocks(document, open_blocks, cur) + cur = -1 + elseif open_blocks[cur].text->len() == 1 + # may be a table + var marker = line->matchstr(table_delimiter) + if !marker->empty() + if open_blocks[cur].text[0]->split('\\\@1len() == marker->split("|")->len() + open_blocks->add(CreateLeafBlock( + "table", + open_blocks->remove(cur).text[0], + marker)) + cur = -1 + endif + endif + endif + break + endif + cur += 1 + endwhile + + if cur < 0 + # the whole line is already consumed + continue + endif + + # a themaic break close all previous blocks + if line =~ thematic_break + CloseBlocks(document, open_blocks) + if &g:encoding == "utf-8" + document.content->add({text: "\u2500"->repeat(80)}) + else + document.content->add({text: "-"->repeat(80)}) + endif + last_block = "hr" + continue + endif + + # check for new container blocks + while v:true + var block = line->matchstrpos($'{block_quote}\|{list_item}') + if block[1] < 0 + break + endif + # close unmatched blocks + CloseBlocks(document, open_blocks, cur) + # start a new block + open_blocks->add(CreateContainerBlock(block, document->len())) + cur = open_blocks->len() + line = line[block[2] :] + endwhile + + # check for leaf block + if line =~ code_fence + CloseBlocks(document, open_blocks, cur) + open_blocks->add(CreateLeafBlock("fenced_code", line)) + elseif line =~ blank_line + if open_blocks->empty() + continue + endif + if open_blocks[-1].type == "paragraph" + CloseBlocks(document, open_blocks, min([cur, open_blocks->len() - 1])) + elseif open_blocks[-1].type == "table" + CloseBlocks(document, open_blocks, open_blocks->len() - 1) + elseif open_blocks[-1].type =~ '_code' + open_blocks[-1].text->add(line) + endif + elseif line =~ code_indent + if open_blocks->empty() + open_blocks->add(CreateLeafBlock("indented_code", line)) + elseif open_blocks[-1].type =~ '_code' + open_blocks[-1].text->add(line->matchstr(code_indent)) + elseif open_blocks[-1].type == "paragraph" + open_blocks[-1].text->add(line->matchstr(paragraph)) + else + CloseBlocks(document, open_blocks, cur) + open_blocks->add(CreateLeafBlock("indented_code", line)) + endif + elseif line =~ atx_heading + CloseBlocks(document, open_blocks, cur) + var token = line->matchlist(atx_heading) + open_blocks->add(CreateLeafBlock("heading", token[2], token[1]->len())) + CloseBlocks(document, open_blocks, cur) + elseif !open_blocks->empty() + if open_blocks[-1].type == "table" + open_blocks[-1].text->add(line) + elseif open_blocks[-1].type == "paragraph" + open_blocks[-1].text->add(line->matchstr(paragraph)) + else + CloseBlocks(document, open_blocks, cur) + open_blocks->add(CreateLeafBlock("paragraph", line)) + endif + else + open_blocks->add(CreateLeafBlock("paragraph", line)) + endif + endfor + + CloseBlocks(document, open_blocks) + return document +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/ftplugin/lspgfm.vim b/ftplugin/lspgfm.vim new file mode 100644 index 0000000..52b63dd --- /dev/null +++ b/ftplugin/lspgfm.vim @@ -0,0 +1,23 @@ +vim9script + +import autoload '../autoload/lsp/markdown.vim' as md + +var bnr: number = bufnr() +var popup_id: number +var document: dict> + +try + popup_id = bnr->getbufinfo()[0].popups[0] + document = md.ParseMarkdown(bnr->getbufline(1, '$')) +catch /.*/ + b:markdown_fallback = v:true + finish +endtry + +b:lsp_syntax = document.syntax +md.list_pattern->setbufvar(bnr, '&formatlistpat') +var settings = 'encoding=utf-8 linebreak breakindent breakindentopt=list:-1' +win_execute(popup_id, $'setlocal {settings}') +popup_id->popup_settext(document.content) + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/syntax/lspgfm.vim b/syntax/lspgfm.vim new file mode 100644 index 0000000..9e71010 --- /dev/null +++ b/syntax/lspgfm.vim @@ -0,0 +1,23 @@ +vim9script + +if get(b:, 'markdown_fallback', v:false) + runtime! syntax/markdown.vim + finish +endif + +var group: dict = {} +for region in get(b:, 'lsp_syntax', []) + if !group->has_key(region.lang) + group[region.lang] = region.lang->substitute('\(^.\|_\a\)', '\u&', 'g') + try + exe $'syntax include @{group[region.lang]} syntax/{region.lang}.vim' + catch /.*/ + group[region.lang] = '' + endtry + endif + if !group[region.lang]->empty() + exe $'syntax region lspCodeBlock start="{region.start}" end="{region.end}" contains=@{group[region.lang]}' + endif +endfor + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 -- 2.48.1