import * as vscode from 'vscode' ; import * as fs from 'fs' ; import { Extension } from '../../extension' ; import { bibtexParser } from 'latex-utensils' ; export interface Suggestion extends vscode . CompletionItem { key : string ; documentation : string ; fields : { [ key ] : string ; } ; file : string ; position : vscode . Position ; } export class Citation { extension : Extension ; private bibEntries : { [ file ] : Suggestion [ ] ; } = { } ; constructor ( extension ) { this . extension = extension ; } provide ( args ? ) { return this . updateAll ( ) . map ( item => { item . filterText = `${ item . key } ${ item . fields . author } ${ item . fields . title } ${ item . fields . journal }` ; item . insertText = item . key ; if ( args ) { item . range = args . document . getWordRangeAtPosition ( args . position , /[-a-zA-Z0-9_:.]+/ ) ; } return item ; } ) ; } browser ( _args ? ) { vscode . window . showQuickPick ( this . updateAll ( ) . map ( item => { return { label : item . fields . title ? item . fields . title : '' , description : `${ item . key }` , detail : `Authors: ${ item . fields . author ? item . fields . author : 'Unknown' }, publication: ${ item . fields . journal ? item . fields . journal : ( item . fields . journaltitle ? item . fields . journaltitle : ( item . fields . publisher ? item . fields . publisher : 'Unknown' ) ) }` } ; } ) , { placeHolder : 'Press ENTER to insert citation key at cursor' , matchOnDetail : true , matchOnDescription : true , ignoreFocusOut : true } ) . then ( selected => { if ( ! selected ) { return ; } if ( vscode . window . activeTextEditor ) { const editor = vscode . window . activeTextEditor ; const content = editor . document . getText ( new vscode . Range ( new vscode . Position ( 0 , 0 ) , editor . selection . start ) ) ; let start = editor . selection . start ; if ( content . lastIndexOf ( '\\cite' ) > content . lastIndexOf ( '}' ) ) { const curlyStart = content . lastIndexOf ( '{' ) + 1 ; const commaStart = content . lastIndexOf ( ',' ) + 1 ; start = editor . document . positionAt ( curlyStart > commaStart ? curlyStart : commaStart ) ; } editor . edit ( edit => edit . replace ( new vscode . Range ( start , editor . selection . start ) , selected . description || '' ) ) . then ( ( ) => editor . selection = new vscode . Selection ( editor . selection . end , editor . selection . end ) ) ; } } ) ; } parseBibFile ( file ) { const fields = ( vscode . workspace . getConfiguration ( 'PandocCiter' ) . get ( 'CitationFormat' ) as string [ ] ) . map ( f => { return f . toLowerCase ( ) ; } ) ; this . extension . log ( `Parsing .bib entries from ${ file }` ) ; this . bibEntries [ file ] = [ ] ; const bibtex = fs . readFileSync ( file ) . toString ( ) ; const ast = bibtexParser . parse ( bibtex ) ; ast . content . filter ( bibtexParser . isEntry ) . forEach ( ( entry ) => { if ( entry . internalKey === undefined ) { return ; } const item = { key : entry . internalKey , label : entry . internalKey , file , position : new vscode . Position ( entry . location . start . line - 1 , entry . location . start . column - 1 ) , kind : vscode . CompletionItemKind . Reference , documentation : '' , fields : { } } ; fields . forEach ( field => { const fieldcontents = entry . content . filter ( e => e . name === field ) ; if ( fieldcontents . length > 0 ) { const fieldcontent = fieldcontents [ 0 ] ; const value = Array . isArray ( fieldcontent . value . content ) ? fieldcontent . value . content . join ( ' ' ) : this . deParenthesis ( fieldcontent . value . content ) ; item . fields [ fieldcontent . name ] = value ; item . documentation += `${ fieldcontent . name . charAt ( 0 ) . toUpperCase ( ) + fieldcontent . name . slice ( 1 ) }: ${ value }\n` ; } } ) ; this . bibEntries [ file ] . push ( item ) ; } ) ; this . extension . log ( `Parsed ${ this . bibEntries [ file ] . length } bib entries from ${ file }.` ) ; } getEntry ( key ) { const suggestions = this . updateAll ( ) ; const entry = suggestions . find ( ( elm ) => elm . key === key ) ; return entry ; } private deParenthesis ( str ) { return str . replace ( /{+([^\\{}]+)}+/g , '$1' ) ; } private updateAll ( bibFiles ? ) { let suggestions = [ ] ; if ( bibFiles === undefined ) { bibFiles = Object . keys ( this . bibEntries ) ; } bibFiles . forEach ( file => { suggestions = suggestions . concat ( this . bibEntries [ file ] ) ; } ) ; this . checkForDuplicates ( suggestions ) ; return suggestions ; } checkForDuplicates ( items ) { const allKeys = ( items . map ( items => items . key ) ) ; if ( ( new Set ( allKeys ) ) . size !== allKeys . length ) { const count = keys => keys . reduce ( ( a , b ) => Object . assign ( a , { [ b ] : ( a [ b ] || 0 ) + 1 } ) , { } ) ; const duplicates = dict => Object . keys ( dict ) . filter ( ( a ) => dict [ a ] > 1 ) ; vscode . window . showInformationMessage ( `Duplicate key(s): ${ duplicates ( count ( allKeys ) ) }` ) ; } } forgetParsedBibItems ( bibPath ) { this . extension . log ( `Forgetting parsed bib entries for ${ bibPath }` ) ; delete this . bibEntries [ bibPath ] ; } }