Skip to content

[WIP] Support for graphviz dot #109

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

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
219 changes: 219 additions & 0 deletions autoload/sj/dot.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
let s:skip = sj#SkipSyntax(['dotString','dotComment'])
let s:edge = '->'


" Callback functions {{{
function! sj#dot#SplitStatement()
if sj#SearchSkip(';\s*\S', s:skip, 'e', line('.'))
execute "normal! dT;i\<CR>"
return 1
else
return 0
endif
endfunction

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

function! sj#dot#SplitChainedEdge()
" FIXME Now sj#dot#SplitStatement does not assert only single line statements afterwards,
" so there might occur an error here. let line = getline('.')
let l:line = getline('.')
if l:line !~ s:edge . '.*' . s:edge | return 0 | endif
let l:statement = s:TrimSemicolon(l:line)
let l:edges = s:ExtractEdges(l:statement)
call map(l:edges, 's:Edge2string(v:val)')
call sj#ReplaceMotion('V', join(l: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 = s: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
" }}}

" Helper functions {{{
" Split multiple nodes into single elements
" INPUT: 'A, B, C'
" OUTPUT: ['A', 'B', 'C']
function! s:ExtractNodes(side)
" 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 l:nodes = split(a:side, ',')
call sj#TrimList(l:nodes)
call uniq(sort(l:nodes))
return l:nodes
endfunction

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

" Extract elements of potentially chained edges as [src,dst] pairs
" INPUT: 'A, B -> C -> D'
" OUTPUT: List of edges [[[A, B], [C]], [[C], [D]]]
function! s:ExtractEdges(statement)
let l: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 l:sides = split(l:statement, s:edge)
if len(l:sides) < 2 | return [] | endif
let [l:edges, l:idx] = [[], 0]
while l:idx < len(l:sides) - 1
" handling of chained expressions
" such as A -> B -> C
let l:edges += [[s:ExtractNodes(get(l:sides, l:idx)),
\ s:ExtractNodes(get(l:sides, l:idx + 1))]]
let l:idx = l:idx + 1
endwhile
return l:edges
endfunction

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

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

" INPUT: [[src_nodes], [dst_nodes]]
" OUTPUT: string representation of the aequivalent statement
function! s:Edge2string(edge)
let l:edge = copy(a:edge)
let l:edge = map(l:edge, 'join(v:val, ", ")')
let l:edge = join(l:edge, ' -> ')
let l:edge = l:edge . ';'
return l:edge
endfunction

" INPUT: Set of potentially mergable edges
" OUTPUT: Set of edges containing multi-edges
function! s:MergeEdges(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

" INPUT: set of potentially transitive edges
" OUTPUT: all transitive edges are merged into chained edges
function! s:ChainTransitiveEdges(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

" }}}
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
43 changes: 43 additions & 0 deletions spec/plugin/dot_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require 'spec_helper'

describe "dot" do
let(:filename) { 'test.dot' }

specify "statements" do
set_file_contents "A, B -> C -> D -> E; X -> Y;"

split

assert_file_contents <<-EOF
A, B -> C -> D -> E;
X -> Y;
EOF

join

assert_file_contents "A, B -> C -> D -> E; X -> Y;"
end

specify "edges" do
set_file_contents "A, B -> C -> D -> E;"

split

assert_file_contents <<-EOF
A, B -> C;
C -> D;
D -> E;
EOF

join

assert_file_contents <<-EOF
A, B -> C -> D;
D -> E;
EOF

join

assert_file_contents "A, B -> C -> D -> E;"
end
end