summaryrefslogblamecommitdiffstats
path: root/autoload/smt2/formatter.vim
blob: 6a461b4c17b7edc1bdaaccc52525875dadd54748 (plain) (tree)













































































































































                                                                                             
if polyglot#init#is_disabled(expand('<sfile>:p'), 'smt2', 'autoload/smt2/formatter.vim')
  finish
endif

" Formatting requires a rather recent Vim version
if !((v:version > 802) || (v:version == 802 && has("patch2725")))
    const s:errmsg_oldvim = "Vim >= 8.2.2725 required for auto-formatting"

    "Dummies
    function! smt2#formatter#FormatCurrentParagraph()
        echoerr s:errmsg_oldvim
    endfunction
    function! smt2#formatter#FormatAllParagraphs()
        echoerr s:errmsg_oldvim
    endfunction

    finish
endif
vim9script

# ------------------------------------------------------------------------------
# Config
# ------------------------------------------------------------------------------
# Length of "short" S-expressions
if !exists("g:smt2_formatter_short_length")
    g:smt2_formatter_short_length = 80
endif

# String to use for indentation
if !exists("g:smt2_formatter_indent_str")
    g:smt2_formatter_indent_str = '  '
endif

# ------------------------------------------------------------------------------
# Formatter
# ------------------------------------------------------------------------------
def FitsOneLine(ast: dict<any>): bool
    # A paragraph with several entries should not be formatted in one line
    if ast.kind ==# 'Paragraph' && len(ast.value) != 1
        return false
    endif
    return ast.pos_to - ast.pos_from < g:smt2_formatter_short_length && !ast.contains_comment
enddef

def FormatOneLine(ast: dict<any>): string
    if ast.kind ==# 'Atom'
        return ast.value.lexeme
    elseif ast.kind ==# 'SExpr'
        var formatted = []
        for expr in ast.value
            call formatted->add(expr->FormatOneLine())
        endfor
        return '(' .. formatted->join(' ') .. ')'
    elseif ast.kind ==# 'Paragraph'
        return ast.value[0]->FormatOneLine()
    endif
    throw 'Cannot format AST node: ' .. string(ast)
    return '' # Unreachable
enddef

def Format(ast: dict<any>, indent = 0): string
    const indent_str = repeat(g:smt2_formatter_indent_str, indent)

    if ast.kind ==# 'Atom'
        return indent_str .. ast.value.lexeme
    elseif ast.kind ==# 'SExpr'
        # Short expression -- avoid line breaks
        if ast->FitsOneLine()
            return indent_str .. ast->FormatOneLine()
        endif

        # Long expression -- break lines and indent subexpressions.
        # Don't break before first subexpression if it's an atom
        # Note: ast.value->empty() == false; otherwise it would fit in one line
        var formatted = []
        if (ast.value[0].kind ==# 'Atom')
            call formatted->add(ast.value[0]->Format(0))
        else
            call formatted->add("\n" .. ast.value[0]->Format(indent + 1))
        endif
        for child in ast.value[1 :]
            call formatted->add(child->Format(indent + 1))
        endfor
        return indent_str .. "(" .. formatted->join("\n") .. ")"
    elseif ast.kind ==# 'Paragraph'
        var formatted = []
        for child in ast.value
            call formatted->add(child->Format())
        endfor
        return formatted->join("\n")
    endif
    throw 'Cannot format AST node: ' .. string(ast)
    return '' # Unreachable
enddef

# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
def smt2#formatter#FormatCurrentParagraph()
    const cursor = getpos('.')
    const ast = smt2#parser#ParseCurrentParagraph()

    # Identify on which end of the buffer we are (to fix newlines later)
    silent! normal! {
    const is_first_paragraph = line('.') == 1
    silent! normal! }
    const is_last_paragraph = line('.') == line('$')

    # Replace paragraph by formatted lines
    const lines = split(Format(ast), '\n')
    silent! normal! {d}
    if is_last_paragraph && !is_first_paragraph
        call append('.', [''] + lines)
    else
        call append('.', lines + [''])
    endif

    # Remove potentially introduced first empty line
    if is_first_paragraph | silent! :1delete | endif

    # Restore cursor position
    call setpos('.', cursor)
enddef

def smt2#formatter#FormatAllParagraphs()
    const cursor = getpos('.')
    const asts = smt2#parser#ParseAllParagraphs()

    # Clear buffer & insert formatted paragraphs
    silent! :1,$delete
    for ast in asts
        const lines = split(Format(ast), '\n') + ['']
        call append('$', lines)
    endfor

    # Remove first & trailing empty lines
    silent! :1delete
    silent! :$delete

    # Restore cursor position
    call setpos('.', cursor)
enddef