-
Notifications
You must be signed in to change notification settings - Fork 90
[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
base: main
Are you sure you want to change the base?
Changes from all commits
5c4112e
94a7868
8684da1
d357192
729cfe4
042c189
a7d979c
a6aef8f
30a3250
5fdcc51
b8490bc
8bacdd0
4e03adb
1b9582d
6095f46
6fd6a28
2b9adf6
49beb62
7728b66
712af9f
8f61383
b207a01
0534e34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"' | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 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 | ||
|
||
" }}} |
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 |
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 |
There was a problem hiding this comment.
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.