/** * LaTeX Completion Provider for Monaco Editor. * Provides autocompletion for LaTeX commands, environments, and packages. */ import type * as Monaco from 'monaco-editor'; import { LATEX_COMMANDS, LATEX_ENVIRONMENTS, LATEX_PACKAGES, type LaTeXCommand } from './commands/data'; export class LaTeXCompletionProvider implements Monaco.languages.CompletionItemProvider { triggerCharacters = ['\\', '{', '[']; constructor(private monaco: typeof Monaco) {} provideCompletionItems( model: Monaco.editor.ITextModel, position: Monaco.Position, context: Monaco.languages.CompletionContext, token: Monaco.CancellationToken ): Monaco.languages.ProviderResult { const textUntilPosition = model.getValueInRange({ startLineNumber: position.lineNumber, startColumn: 1, endLineNumber: position.lineNumber, endColumn: position.column, }); const suggestions: Monaco.languages.CompletionItem[] = []; // Check if we're after a backslash (command completion) const commandMatch = textUntilPosition.match(/\\([\w]*)$/); if (commandMatch) { // Calculate range for just the command name (after backslash) const commandRange = { startLineNumber: position.lineNumber, startColumn: position.column - commandMatch[1].length, endLineNumber: position.lineNumber, endColumn: position.column, }; suggestions.push(...this.getCommandCompletions(commandRange)); } // Check if we're inside \begin{ or \end{ (environment completion) const envMatch = textUntilPosition.match(/\\(begin|end)\{([\w*]*)$/); if (envMatch) { const envRange = { startLineNumber: position.lineNumber, startColumn: position.column - envMatch[2].length, endLineNumber: position.lineNumber, endColumn: position.column, }; suggestions.push(...this.getEnvironmentCompletions(envRange)); } // Check if we're inside \usepackage{ (package completion) const pkgMatch = textUntilPosition.match(/\\usepackage(?:\[.*?\])?\{([\w,\s]*)$/); if (pkgMatch) { const pkgRange = { startLineNumber: position.lineNumber, startColumn: position.column - pkgMatch[1].length, endLineNumber: position.lineNumber, endColumn: position.column, }; suggestions.push(...this.getPackageCompletions(pkgRange)); } return { suggestions }; } private getCommandCompletions(range: Monaco.IRange): Monaco.languages.CompletionItem[] { return LATEX_COMMANDS.map((cmd, index) => ({ label: `\\${cmd.name}`, kind: this.monaco.languages.CompletionItemKind.Function, // insertText without backslash since we only replace the command name insertText: cmd.snippet || cmd.name, insertTextRules: cmd.snippet ? this.monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet : undefined, detail: cmd.signature ? `\\${cmd.name}${cmd.signature}` : cmd.category, documentation: this.formatDocumentation(cmd), range, sortText: String(index).padStart(4, '0'), // Filter based on what user typed after backslash filterText: cmd.name, })); } private getEnvironmentCompletions(range: Monaco.IRange): Monaco.languages.CompletionItem[] { return LATEX_ENVIRONMENTS.map((env, index) => ({ label: env.name, kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: env.name, detail: env.package ? `Requires: ${env.package}` : 'Base LaTeX', documentation: { value: env.description, }, range, sortText: String(index).padStart(4, '0'), })); } private getPackageCompletions(range: Monaco.IRange): Monaco.languages.CompletionItem[] { return LATEX_PACKAGES.map((pkg, index) => ({ label: pkg.name, kind: this.monaco.languages.CompletionItemKind.Module, insertText: pkg.name, documentation: pkg.description, range, sortText: String(index).padStart(4, '0'), })); } private formatDocumentation(cmd: LaTeXCommand): Monaco.IMarkdownString { let doc = `**${cmd.description}**\n\n`; if (cmd.package) { doc += `*Requires:* \`\\usepackage{${cmd.package}}\`\n\n`; } if (cmd.example) { doc += `**Example:**\n\`\`\`latex\n${cmd.example}\n\`\`\``; } return { value: doc }; } } /** * Creates a completion provider for LaTeX. */ export function createCompletionProvider(monaco: typeof Monaco): LaTeXCompletionProvider { return new LaTeXCompletionProvider(monaco); }