Skip to content

Commit

Permalink
Merge pull request #4491 from James-Yu/4472_bib_glossary
Browse files Browse the repository at this point in the history
Parse glossary bib files to populate intellisense
  • Loading branch information
jlelong authored Dec 26, 2024
2 parents 63e1ebc + 4f0274f commit c3a5952
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 75 deletions.
36 changes: 6 additions & 30 deletions src/completion/completer/citation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ export const bibTools = {
parseAbbrevations
}

function expandField(abbreviations: {[key: string]: string}, value: bibtexParser.FieldValue): string {
function expandField(abbreviations: {[key: string]: string}, value: bibtexParser.FieldValue | undefined): string {
if (value === undefined) {
return ''
}
if (value.kind === 'concat') {
const args = value.content as bibtexParser.FieldValue[]
return args.map(arg => expandField(abbreviations, arg)).join(' ')
Expand Down Expand Up @@ -97,7 +100,7 @@ function provide(uri: vscode.Uri, line: string, position: vscode.Position): Comp
const label = configuration.get('intellisense.citation.label') as string
const fields = readCitationFormat(configuration)
const range: vscode.Range | undefined = computeFilteringRange(line, position)
return updateAll(getIncludedBibs(lw.root.file.path)).map(item => {
return updateAll(lw.cache.getIncludedBib(lw.root.file.path)).map(item => {
// Compile the completion item label
switch(label) {
case 'bibtex key':
Expand Down Expand Up @@ -128,7 +131,7 @@ function browser(args?: CompletionArgs) {
const configuration = vscode.workspace.getConfiguration('latex-workshop', args?.uri)
const label = configuration.get('intellisense.citation.label') as string
const fields = readCitationFormat(configuration, label)
void vscode.window.showQuickPick(updateAll(getIncludedBibs(lw.root.file.path)).map(item => {
void vscode.window.showQuickPick(updateAll(lw.cache.getIncludedBib(lw.root.file.path)).map(item => {
return {
label: item.fields.title ? trimMultiLineString(item.fields.title) : '',
description: item.key,
Expand Down Expand Up @@ -175,33 +178,6 @@ function getItem(key: string, configurationScope?: vscode.ConfigurationScope): C
return entry
}

/**
* Returns the array of the paths of `.bib` files referenced from `file`.
*
* @param file The path of a LaTeX file. If `undefined`, the keys of `bibEntries` are used.
* @param visitedTeX Internal use only.
*/
function getIncludedBibs(file?: string, visitedTeX: string[] = []): string[] {
if (file === undefined) {
// Only happens when rootFile is undefined
return Array.from(data.bibEntries.keys())
}
const cache = lw.cache.get(file)
if (cache === undefined) {
return []
}
let bibs = Array.from(cache.bibfiles)
visitedTeX.push(file)
for (const child of cache.children) {
if (visitedTeX.includes(child.filePath)) {
// Already included
continue
}
bibs = Array.from(new Set(bibs.concat(getIncludedBibs(child.filePath, visitedTeX))))
}
return bibs
}

/**
* Returns aggregated bib entries from `.bib` files and bibitems defined on LaTeX files included in the root file.
*
Expand Down
125 changes: 102 additions & 23 deletions src/completion/completer/glossary.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
import * as vscode from 'vscode'
import type * as Ast from '@unified-latex/unified-latex-types'
import { bibtexParser } from 'latex-utensils'
import { lw } from '../../lw'
import { GlossaryType } from '../../types'
import type { CompletionProvider, FileCache, GlossaryItem } from '../../types'
import { argContentToStr } from '../../utils/parser'
import { getLongestBalancedString } from '../../utils/utils'
import { bibTools } from './citation'

const logger = lw.log('Intelli', 'Glossary')
export const provider: CompletionProvider = { from }
export const glossary = {
parse,
getItem
getItem,
parseBibFile
}

const data = {
// The keys are the labels of the glossary items.
glossaries: new Map<string, GlossaryItem>(),
acronyms: new Map<string, GlossaryItem>()
acronyms: new Map<string, GlossaryItem>(),
// The keys are the paths of the `.bib` files.
bibEntries: new Map<string, GlossaryItem[]>()
}

interface GlossaryEntry {
label: string | undefined,
description: string | undefined
}
lw.watcher.bib.onCreate(uri => parseBibFile(uri.fsPath))
lw.watcher.bib.onChange(uri => parseBibFile(uri.fsPath))
lw.watcher.bib.onDelete(uri => removeEntriesInFile(uri.fsPath))

function from(result: RegExpMatchArray): vscode.CompletionItem[] {
updateAll()
updateAll(lw.cache.getIncludedGlossaryBib(lw.root.file.path))
let suggestions: Map<string, GlossaryItem>

if (result[1] && result[1].match(/^ac/i)) {
Expand All @@ -38,14 +44,33 @@ function from(result: RegExpMatchArray): vscode.CompletionItem[] {
}

function getItem(token: string): GlossaryItem | undefined {
updateAll()
updateAll(lw.cache.getIncludedGlossaryBib(lw.root.file.path))
return data.glossaries.get(token) || data.acronyms.get(token)
}

function updateAll() {

/**
* Returns aggregated glossary entries from `.bib` files and glossary items defined on LaTeX files included in the root file.
*
* @param bibFiles The array of the paths of `.bib` files. If `undefined`, the keys of `bibEntries` are used.
*/
function updateAll(bibFiles: string[]) {
// Extract cached references
const glossaryList: string[] = []

// From bib files
bibFiles.forEach(file => {
const entries = data.bibEntries.get(file)
entries?.forEach(entry => {
if (entry.type === GlossaryType.glossary) {
data.glossaries.set(entry.label, entry)
} else {
data.acronyms.set(entry.label, entry)
}
glossaryList.push(entry.label)
})
})

lw.cache.getIncludedTeX().forEach(cachedFile => {
const cachedGlossaries = lw.cache.get(cachedFile)?.elements.glossary
if (cachedGlossaries === undefined) {
Expand All @@ -61,7 +86,7 @@ function updateAll() {
})
})

// Remove references that has been deleted
// Remove references that have been deleted
data.glossaries.forEach((_, key) => {
if (!glossaryList.includes(key)) {
data.glossaries.delete(key)
Expand All @@ -74,6 +99,64 @@ function updateAll() {
})
}

/**
* Parse a glossary `.bib` file. The results are stored in this instance.
*
* @param fileName The path of `.bib` file.
*/
async function parseBibFile(fileName: string) {
logger.log(`Parsing glossary .bib entries from ${fileName}`)
const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(fileName))
if ((await lw.external.stat(vscode.Uri.file(fileName))).size >= (configuration.get('bibtex.maxFileSize') as number) * 1024 * 1024) {
logger.log(`Bib file is too large, ignoring it: ${fileName}`)
data.bibEntries.delete(fileName)
return
}
const newEntry: GlossaryItem[] = []
const bibtex = await lw.file.read(fileName)
logger.log(`Parse BibTeX AST from ${fileName} .`)
const ast = await lw.parser.parse.bib(vscode.Uri.file(fileName), bibtex ?? '')
if (ast === undefined) {
logger.log(`Parsed 0 bib entries from ${fileName}.`)
lw.event.fire(lw.event.FileParsed, fileName)
return
}
const abbreviations = bibTools.parseAbbrevations(ast)
ast.content
.filter(bibtexParser.isEntry)
.forEach((entry: bibtexParser.Entry) => {
if (entry.internalKey === undefined) {
return
}
let type: GlossaryType
if ( ['entry'].includes(entry.entryType) ) {
type = GlossaryType.glossary
} else {
type = GlossaryType.acronym
}
const name = bibTools.expandField(abbreviations, entry.content.find(field => field.name === 'name')?.value)
const description = bibTools.expandField(abbreviations, entry.content.find(field => field.name === 'description')?.value)
const item: GlossaryItem = {
type,
label: entry.internalKey,
filePath: fileName,
position: new vscode.Position(entry.location.start.line - 1, entry.location.start.column - 1),
kind: vscode.CompletionItemKind.Reference,
detail: name + ': ' + description
}
newEntry.push(item)
})
data.bibEntries.set(fileName, newEntry)
logger.log(`Parsed ${newEntry.length} glossary bib entries from ${fileName} .`)
void lw.outline.reconstruct()
lw.event.fire(lw.event.FileParsed, fileName)
}

function removeEntriesInFile(file: string) {
logger.log(`Remove parsed bib entries for ${file}`)
data.bibEntries.delete(file)
}

function parse(cache: FileCache) {
if (cache.ast !== undefined) {
cache.elements.glossary = parseAst(cache.ast, cache.filePath)
Expand All @@ -84,12 +167,13 @@ function parse(cache: FileCache) {

function parseAst(node: Ast.Node, filePath: string): GlossaryItem[] {
let glos: GlossaryItem[] = []
let entry: GlossaryEntry = { label: '', description: '' }
let label: string = ''
let description: string = ''
let type: GlossaryType | undefined

if (node.type === 'macro' && ['newglossaryentry', 'provideglossaryentry'].includes(node.content)) {
type = GlossaryType.glossary
let description = argContentToStr(node.args?.[1]?.content || [], true)
description = argContentToStr(node.args?.[1]?.content || [], true)
const index = description.indexOf('description=')
if (index >= 0) {
description = description.slice(index + 12)
Expand All @@ -101,28 +185,23 @@ function parseAst(node: Ast.Node, filePath: string): GlossaryItem[] {
} else {
description = ''
}
entry = {
label: argContentToStr(node.args?.[0]?.content || []),
description
}
label = argContentToStr(node.args?.[0]?.content || [])
} else if (node.type === 'macro' && ['longnewglossaryentry', 'longprovideglossaryentry', 'newacronym', 'newabbreviation', 'newabbr'].includes(node.content)) {
if (['longnewglossaryentry', 'longprovideglossaryentry'].includes(node.content)) {
type = GlossaryType.glossary
} else {
type = GlossaryType.acronym
}
entry = {
label: argContentToStr(node.args?.[1]?.content || []),
description: argContentToStr(node.args?.[3]?.content || []),
}
label = argContentToStr(node.args?.[1]?.content || [])
description = argContentToStr(node.args?.[3]?.content || [])
}
if (type !== undefined && entry.label && entry.description && node.position !== undefined) {
if (type !== undefined && label && description && node.position !== undefined) {
glos.push({
type,
filePath,
position: new vscode.Position(node.position.start.line - 1, node.position.start.column - 1),
label: entry.label,
detail: entry.description,
label,
detail: description,
kind: vscode.CompletionItemKind.Reference
})
}
Expand Down
Loading

0 comments on commit c3a5952

Please sign in to comment.