Skip to content
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

Parse glossary bib files to populate intellisense #4491

Merged
merged 6 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading