9. lokakuu 2010, 20:03

LaTeX-viittaukset Vimissä

Kirjailin taas uuden palan korostamaan, miten hienosti saa Vimistä irti kaikenlaista. LaTeXissa ja TeXissä yleensäkin viittailessa omaan dokumenttiin halutaan yleensä käyttää komento \label{nimiö} ja sitten siihen voidaan viitata komennolla \ref{nimiö}. Tämä yleensä palauttaa numeroidun otsikon, listan kohdan tai jonkun muun juoksevan numeron. PDF-LaTeX vielä lisää hyperlinkin. Lisäksi on \pageref{nimiö}, joka antaa suoran sivunumeron sille sivulle, missä annettu \label sijaitsee.

Eli idea on kiva. Systeemiä kannattaa isoissa dokumenteissa hieman vielä parantaa siten, että erityyppisiin kohteisiin viittaavat labelit nimetään systemaattisesti, esimerkiksi omissa töissäni:

  • sec: -etuliite otsikoille (section)
  • fig: kuville (figures)
  • tab: taulukoille (tables, tabular)
  • eq: lausekkeille

Semanttisuuden nimissä jaottelua saa toki jatkaa mielinmäärin, eikä nimiöiden nimeämisessä ole juurikaan rajoitteita.

Mutta sitten ongelmana on, että nimiöitä voi olla aika paljon, ja niiden yksikäsitteinen nimeäminen vaatii vähän vaivaa, ja kaikkien muistaminen on hankalaa. Olisipa se poikaa antaa Vimin hoitaa muistamiset puolestani. Käyttäjän kontolle jää hyvien, kuvaavien nimiöiden keksiminen. Pituudella ei ole enää väliä, koska vimin automaattitäydennys hoitaa homman kotiin vähillä näppäilyillä.

Toteutus

Ensin otetaan omnicompletion-funktioiden perusrunko esille:

function! CompleteTeXLabels(findstart, base)
    " findstart == 1 when this is called for the first time
    if a:findstart
        let line = getline('.')
        let start = col('.') - 1
        while start > 0 && line[start - 1] =~ '\a'
            let start -= 1
        endwhile
        return start
    else
        let labels = []
        " haetaan sopivat nimiöt tässä kohtaa
        return labels
    endif
endfun

Kullakin LaTeX-koodin rivillä voi olla nolla, yksi tai useampia \label-määrittelyjä. Laitetaan Vim matchaamaan jokaista riviä nimiöiden toivossa. Selkeyttääkseni olen kirjoittanut yksittäisen rivin tutkinnan omaksi metodikseen:

fun! FindLabels(line) 
    let matches = []
    let i = 1
    let lm = matchlist(a:line, '\v\\label\s*\{([^}]+)}{-1}')
    while !empty(lm)
        call add(matches, lm[1])
        let i += 1
        let lm = matchlist(a:line, '\v\\label\s*\{([^}]+)}{-1}', 0, i)
    endwhile
    return matches
endfun

Nyt ollaan nätisti asian ytimessä! Säännöllinen lauseke oli melko paskamainen saada kuntoon, sillä jostain syystä \{(.+)\}{-1} ei toimi, kuten luulisi. Vimin lausekkeissa miinusmerkkinen haku tarkoittaa, että matchaus loppuu ensimmäiseen löytyneeseen osumaan, siis non-greedyyn tapaan. Kuitenkin tämä jätti toimimatta (ottanen yhteyttä asian tiimoilta Vimin postilistoille) ja jouduin käyttämään kirjainluokkaa, joka rajaa }-merkit sisältä pois. Ei iso omissio, mutta kaipa LaTeXissa joku haluaisi käyttää labeleita tyyliin \label{Nice and big\{not\}}, joka ei enää toimisi skriptissäni.

Muuten FindLabels toimii kuten odottaa saattaa. Varsinainen toiminnallisuus nyt ujutetaan tuonne CompleteTeXLabels -funktion sisään:

let line = line('.')
while line >= 0
    let laline = FindLabels(getline(line))
    let line -= 1
    for label in laline
        if label =~ '^' . a:base
            call add(labels, label)
        endif
    endfor
endwhile

Tämä on ensimmäinen raakaversio toiminnallisuudesta. Se tekee yhden ison omission: labeleita haetaan vain kutsuriviltä ja taaksepäin, aina tiedoston alkuun. Eli jos teet uuden labelin tiedoston lopuille ja haluat reffin keskelle, ei tämä ensiversio toimi. Syynä on se, että aluksi olin skeptinen tämän kokonaisuuden suorituskyvystä. Melko raskasta parsia koko tiedostoa aina kun halutaan löytää nimiöitä.

Kuitenkin havaitsin homman sulavaksi isoillakin tiedostoilla, 4000-rivisilläkin. Jos lisään nyt ketjuun toisen puoliskon, saadaan ainakin senhetkinen bufferi katettua. Sopivalla tavalla järjestelemällä haut saadaan aikaan systeemi, missä ensimmäisenä täydennyslistalla ovat nimiöt ylöspäin ja sitten nimiöt alaspäin. Koodiin lisätään siis toinen silmukka, aika- tai tilavaativuus ei kasva.

        " forwards
        let line = line('.') + 1
        let eol = line('$') 
        while line <= eol
            let laline = FindLabels(getline(line))
            let line += 1
            for label in laline
                if label =~ '^' . a:base
                    call add(labels, label)
                endif
            endfor
        endwhile

Ongelmana on kuitenkin se, että isommat LaTeX-tarinoinnit kannattaa hallinnollisista syistä jakaa useisiin tiedostoihin. Toteutuksen voisi tehdä siten, että haku tehdään kaikista aukiolevista tex-päätteellisistä tiedostoista. Niin, niin sen voisi tehdä.

Sitä varten taitaa kannattaa tehdä jo taas jakoa useampaan funktioon. Lopulta varsinainen omnifunktio koostuu seuraavanlaisesta lihasta:

        let labels = []
        " go through all buffers
        for bufnum in range(1, bufnr('$'))
            if bufname(bufnum) =~ "\.tex$"
                let labels +=  FindLabels_Buffer(bufnum, a:base)
            endif
        endfor
        return labels

Tässä koodissa FindLabels_Buffer tekee laajalti osin saman, mitä aiemmissa, yhtä bufferia koskevissa toteutuksissa tehdään. Siitä toteutuksesta tuli varsin sotkuinen, joten toistaiseksi jätän sen toteutuksen harjoitustehtäväksi. ;) Se kuitenkin sotkuisuudessaan toteuttaa alkuperäisen idean, että aktiivisen bufferin sisältöä käydään ensin kutsuriviltä ylöspäin, sitten alaspäin, ja sitten muut puskurit rivijärjestyksessä. En äkkiseltään löytänyt suoraa ratkaisua sille, miten haetaan tietyn puskurin rivimäärä, joten jouduin käyttämään potentiaalisesti kallista metodia, joka palauttaa koko puskurin rivit listana. Hyvässä tapauksessa se on kytketty suoraan Vimin sisäiseen tiedostototeutukseen, huonossa tapauksessa jokainen puskuri kopioidaan muistiin toista kertaa, hitaasti.

Toteutus on edelleen (!) nopea, vaikka alunperin ajattelin jonkun cachetuksen järjestää. Ei tunnukaan olevan tarvetta.

Tageja: , ,

---
---

---

Aiheen vierestä