Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Support for graphviz dot #109

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions autoload/sj/dot.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
let s:edge = '->'
" node regexp unused
let s:node = '\("*[^\"]\{-}"\|\i\+\)'

" Helper functions {{{
function! sj#dot#ExtractNodes(side)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I like to keep the helper functions at the bottom of the file, after the public interface. I also prefer to keep them script-local (starting with s:) most of the time. I only use sj#whatever#Func for functions that should be shared across different files. I think this one (and a few more) can be script-local.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

" Split multiple nodes into single elements
" INPUT: 'A, B, C'
" OUTPUT: ['A', 'B', 'C']
" FIXME will fail on 'A, B, "some,label"'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use an argparser for this. This JSON one might work out of the box: https://github.com/AndrewRadev/splitjoin.vim/blob/6095f461651c2416cc31b52039806b9e52428388/autoload/sj/argparser/js.vim.

The challenge would be restructuring your code so you don't provide a string, but an area of the buffer. For more complicated processing, sadly, just taking the string doesn't work out well in practice -- in the buffer, you can move the cursor, and you can check syntax items under it.

let nodes = split(a:side, ',')
call sj#TrimList(nodes)
call uniq(sort(nodes))
return nodes
endfunction

function! s:TrimSemicolon(statement)
return substitute(a:statement, ';$', '', '')
endfunction

function! sj#dot#ExtractEdges(statement)
" Extract elements of potentially chained edges as [src,dst] pairs
" INPUT: 'A, B -> C -> D'
" OUTPUT: [[[A, B], [C]], [[C], [D]]]
let statement = s:TrimSemicolon(a:statement)
" FIXME will fail if '->' inside "s
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's two ways we can avoid this issue. One is to check the syntax under the cursor. The syntax group under the arrow, for me at least, is dotKeyChar. You can use the sj#SearchSkip function to search for arrows that only fit this syntax pattern. Grep through the codebase for usage examples.

Alternatively, you could use an argparser. The json one is the most common one I use, and I think you can just replace the check for a comma with a check for ->. It works by "inheriting" a "class" with some common code: https://github.com/AndrewRadev/splitjoin.vim/blob/6095f461651c2416cc31b52039806b9e52428388/autoload/sj/argparser/common.vim

You can probably use the parser for the comma-separated entries for the nodes as well. I'll comment on the other fixme.

let sides = split(statement, s:edge)
if len(sides) < 2 | return [] | endif
let [edges, idx] = [[], 0]
while idx < len(sides) - 1
" handling of chained expressions
" such as A -> B -> C
let edges += [[sj#dot#ExtractNodes(get(sides, idx)),
\ sj#dot#ExtractNodes(get(sides, idx + 1))]]
let idx = idx + 1
endwhile
return edges
endfunction

function! s:ParseConsecutiveLines(...)
" OUTPUT: Either [edges, 0] when 2 statements on first line, else [edges, 1]
" when two statements on two lines

" Safety guard, because multiple statements are not handled at the moment
let statements = split(getline('.'), ';')
if len(statements) > 2
return [[], 0]
elseif len(statements) == 2
" only if exactly 2 edges in one line, else replacemotion fails (atm)
let edges = sj#dot#ExtractEdges(statements[0]) +
\ sj#dot#ExtractEdges(statements[1])
return [edges, 0]
elseif len(statements) == 0
return [[], 0]
endif
" Exactly one statement found on the first lien
" Try to eat the next line

call sj#PushCursor()
" FIXME Dangerous on EOF?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can check for the end of file by consulting line('$'). Probably, if line('.') + 1 is equal to line('$'), you can't really do anything and you might as well return a "failure" (whatever constitutes a failed result here).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normal! j
let statements2 = split(getline('.'), ';')
if len(statements2) > 1
return [[], 1]
endif
let edges = sj#dot#ExtractEdges(statements[0]) +
\ sj#dot#ExtractEdges(statements2[0])
call sj#PopCursor()
return [edges, 1]
endfunction

function! s:Edge2string(edge)
" INPUT: [[src_nodes], [dst_nodes]]
" OUTPUT: string representation of the aequivalent statement
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A very minor thing, but I think I'd prefer these on top of the function definition. Honestly, there's no practical reason for it other than consistency with existing code. I do like the comments themselves, though, quite useful.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

49beb62 addressed this.

let edge = copy(a:edge)
let edge = map(edge, 'join(v:val, ", ")')
let edge = join(edge, ' -> ')
let edge = edge . ';'
return edge
endfunction

function! s:MergeEdges(edges)
" INPUT: Set of potentially mergable edges
" OUTPUT: Set of edges containing multi-edges
let edges = copy(a:edges)
let finished = 0
for [src_nodes, dst_nodes] in edges
call uniq(sort(src_nodes))
call uniq(sort(dst_nodes))
endfor
" all node sets sorted
call uniq(sort(edges))
" all edges sorted
while !finished
let finished = 1
let idx = 0
while idx < len(edges)
let [source_nodes, dest_nodes] = edges[idx]
let jdx = idx + 1
while jdx < len(edges)
if source_nodes == edges[jdx][0]
let dest_nodes += edges[jdx][1]
call uniq(sort(dest_nodes))
let finished = 0
elseif dest_nodes == edges[jdx][1]
let source_nodes += edges[jdx][0]
call uniq(sort(source_nodes))
let finished = 0
endif
if !finished
unlet edges[jdx]
else
let jdx += 1
endif
endwhile
let idx = idx + 1
endwhile
call uniq(sort(edges))
endwhile
return edges
endfunction

function! s:ChainTransitiveEdges(edges)
" INPUT: set of potentially transitive edges
" OUTPUT: all transitive edges are merged into chained edges
let edges = copy(a:edges)
let finished = 0
while !finished
let finished = 1
let idx = 0
while idx < len(edges)
let jdx = idx + 1
while jdx < len(edges)
if edges[idx][-1] == edges[jdx][0]
let edges[idx] += [edges[jdx][-1]]
let finished = 0
unlet edges[jdx]
break
endif
let jdx += 1
endwhile
let idx += 1
endwhile
endwhile
return edges
endfunction

" }}}
" Callback functions {{{
function! sj#dot#SplitStatement()
let statements = split(getline('.'), ';')
if len(statements) < 2 | return 0 | endif
call map(statements, 'v:val . ";"')
call sj#ReplaceMotion('V', join(statements, "\n"))
return 1
endfunction

function! sj#dot#JoinStatement()
" TODO guard for comments etc
normal! J
return 1
endfunction

function! sj#dot#SplitChainedEdge()
let line = getline('.')
if line !~ s:edge . '.*' . s:edge | return 0 | endif
let statement = s:TrimSemicolon(line)
let edges = sj#dot#ExtractEdges(statement)
call map(edges, 's:Edge2string(v:val)')
call sj#ReplaceMotion('V', join(edges, "\n"))
return 1
endfunction

function! sj#dot#JoinChainedEdge()
" TODO initial guard
let [edges, ate] = s:ParseConsecutiveLines()
let edges = s:ChainTransitiveEdges(edges)
" should not be more than one, but also not zero
if len(edges) != 1 | return 0 | endif
let edge_string = s:Edge2string(edges[0])
call sj#ReplaceMotion(ate ? 'Vj' : 'V', edge_string)
return 1
endfunction

function! sj#dot#SplitMultiEdge()
" chop off potential trailing ';'
let statement = substitute(getline('.'), ';$', '', '')
let edges = sj#dot#ExtractEdges(statement)
if !len(edges) | return 0 | endif
" Note that this is something else than applying map -> Edge2string
" since we need to expand all-to-all property of multi-edges
let new_edges = []
for edge in edges
let [lhs, rhs] = edge
for source_node in lhs
for dest_node in rhs
let new_edges += [s:Edge2string([[source_node], [dest_node]])]
endfor
endfor
endfor
let body = join(new_edges, "\n")
call sj#ReplaceMotion('V', body)
return 1
endfunction

function! sj#dot#JoinMultiEdge()
" TODO guard for comments or blank lines
" Check whether two lines are
let [edges, ate] = s:ParseConsecutiveLines()
if len(edges) < 2 | return 0 | endif
let edges = s:MergeEdges(edges)
if len(edges) != 1 | return 0 | endif
call sj#ReplaceMotion(ate ? 'Vj' : 'V', s:Edge2string(edges[0]))
return 1
endfunction
" }}}
15 changes: 15 additions & 0 deletions ftplugin/dot/splitjoin.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
if !exists('b:splitjoin_split_callbacks')
let b:splitjoin_split_callbacks = [
\ 'sj#dot#SplitStatement',
\ 'sj#dot#SplitChainedEdge',
\ 'sj#dot#SplitMultiEdge'
\ ]
endif

if !exists('b:splitjoin_join_callbacks')
let b:splitjoin_join_callbacks = [
\ 'sj#dot#JoinMultiEdge',
\ 'sj#dot#JoinChainedEdge',
\ 'sj#dot#JoinStatement'
\ ]
endif