-
Notifications
You must be signed in to change notification settings - Fork 89
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
base: main
Are you sure you want to change the base?
Changes from 15 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,216 @@ | ||
let s:edge = '->' | ||
" node regexp unused | ||
let s:node = '\("*[^\"]\{-}"\|\i\+\)' | ||
|
||
" Helper functions {{{ | ||
function! sj#dot#ExtractNodes(side) | ||
" Split multiple nodes into single elements | ||
" INPUT: 'A, B, C' | ||
" OUTPUT: ['A', 'B', 'C'] | ||
" FIXME will fail on 'A, B, "some,label"' | ||
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. 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 | ||
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 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? | ||
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. You can check for the end of file by consulting 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. |
||
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 | ||
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. 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. 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. 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 | ||
" }}} |
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 |
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.
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 usesj#whatever#Func
for functions that should be shared across different files. I think this one (and a few more) can be script-local.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.
6fd6a28