import * as vscode from 'vscode'; import {latex_types} from '../dictionary/symbol_types'; import preamble_symbols from '../dictionary/preamble_symbols'; import text_symbols from '../dictionary/text_symbols'; import math_symbols from '../dictionary/math_symbols'; import package_symbols from '../dictionary/package_symbols'; import environment_symbols from '../dictionary/environment_symbols'; import documentclass_symbols from '../dictionary/documentclass_symbols'; import tikz_symbols from '../dictionary/tikz_symbols'; import parameter_dictionary from '../dictionary/parameter_dictionary'; import * as child_process from 'child_process'; function convertToItemKind(type: latex_types): vscode.CompletionItemKind { switch (type) { case latex_types.symbol: return vscode.CompletionItemKind.Variable; case latex_types.function: return vscode.CompletionItemKind.Function; case latex_types.environment: return vscode.CompletionItemKind.Interface; case latex_types.package: return vscode.CompletionItemKind.Module; case latex_types.keyword: return vscode.CompletionItemKind.Keyword; case latex_types.parameter: return vscode.CompletionItemKind.Value; case latex_types.snippet: return vscode.CompletionItemKind.Snippet; default: return vscode.CompletionItemKind.Property; } } function createCompletionItem(label: string, sym_def: Object, match?: string, range?: vscode.Range): vscode.CompletionItem { var item = new vscode.CompletionItem(label, convertToItemKind(sym_def["kind"])); if (sym_def["detail"]) { item.detail = sym_def["detail"]; } if (sym_def["documentation"]) { item.documentation = sym_def["documentation"]; } if (sym_def["insertText"]) { if (sym_def["insertText"].indexOf("$") > -1) { item.insertText = new vscode.SnippetString(sym_def["insertText"]); } else { item.insertText = sym_def["insertText"]; } } if (match) { // if the sortText ins't set, default behaviour ist sorting by label let index = label.indexOf(match); item.sortText = (index > -1 ? index : "") + label; } if (range) { // if no range is set, no replacement will be done just adding the insertText (or label) at cursor position item.range = range; } return item; } function fillSymbols(symbolsCollection: vscode.CompletionItem[], symbolsDefinition: Object, match?: string, position?: vscode.Position): void { if (match) { // if a match is provided, add a sortText for preordering by match index if (position) { var range = new vscode.Range(new vscode.Position(position.line, position.character - match.length), position); } else { throw new Error("[ CompletionItemProvider | fillSymbols ] Parameter \"position\" not supplied. If parameter \"match\" is supplied, \"position\" has to be supplied to!"); } } for (var key in symbolsDefinition) { if (match && !key.includes(match)) { continue; } // if no match is provided, ignore filtering otherwise ignore the symbol if the match isn't contained var sym_def = symbolsDefinition[key]; symbolsCollection.push(createCompletionItem(key, sym_def, match, range)); if (sym_def.additionalInserts instanceof Array) { for (var i = 0; i < sym_def.additionalInserts.length; i++) { var additionalInsert = sym_def.additionalInserts[i]; if (typeof additionalInsert === "string") { sym_def.insertText = additionalInsert; symbolsCollection.push(createCompletionItem(additionalInsert, sym_def, match, range)); } else if (typeof additionalInsert === "object") { sym_def.insertText = additionalInsert["insertText"]; if (additionalInsert.hasOwnProperty("kind")) { sym_def.kind = additionalInsert.kind; } symbolsCollection.push(createCompletionItem(additionalInsert["label"], sym_def, match, range)); } else { throw new Error("[ CompletionItemProvider | fillSymbols ] A additionalInsert instance isn't well defined: " + additionalInsert); } } } } } export default class Provider implements vscode.CompletionItemProvider { private preamble_symbols: vscode.CompletionItem[]; private text_symbols: vscode.CompletionItem[]; private math_symbols: vscode.CompletionItem[]; private package_symbols: vscode.CompletionItem[]; private documentclass_symbols: vscode.CompletionItem[]; private environment_symbols: vscode.CompletionItem[]; private tikz_symbols: vscode.CompletionItem[]; private parameter_dictionary: Object; private environment_type_dict = { "displaymath": "math", "equation": "math", "eqnarray": "math", "align": "math", "align*": "math", "multline": "math", "multline*": "math", "gather": "math", "gather*": "math", "split": "math", "split*": "math", "tikzpicture": "tikz", "document": "text" }; constructor() { // initialize Collections this.preamble_symbols = new Array(); this.text_symbols = new Array(); this.math_symbols = new Array(); this.package_symbols = new Array(); this.environment_symbols = new Array(); this.documentclass_symbols = new Array(); this.tikz_symbols = new Array(); // fill the collections from the LaTeX symbols definitions fillSymbols(this.preamble_symbols, preamble_symbols); fillSymbols(this.text_symbols, text_symbols); fillSymbols(this.math_symbols, math_symbols); fillSymbols(this.package_symbols, package_symbols); fillSymbols(this.environment_symbols, environment_symbols); fillSymbols(this.documentclass_symbols, documentclass_symbols); fillSymbols(this.tikz_symbols, tikz_symbols); this.parameter_dictionary = parameter_dictionary; } dispose() { } private getEnvironmentType(document: vscode.TextDocument, line_before_pos: string, position: vscode.Position): string { { // check for inline math let count = 0; let i: number; for (i = 0; i < line_before_pos.length; i++) { if (line_before_pos[i] === "$") { count++; // count number of inline math beginnings and endings } } if (count % 2 === 1) { // 2 $ characters represent a closed inline math environment -> odd number is in inline math return "math"; // inside inline math -> environment type is math } } // search environment beginings and endings and return type according to environment type var begin_index = -1; var end_index = -1; var environment: string; for (var i = position.line; i >= 0; i--) { // iterate over lines from the current line (position) to the top of he document var line = document.lineAt(i); // get line with linenumber i (0 based) if (line.isEmptyOrWhitespace) { continue; } // consistency check var line_text = line.text; // get the current line as string while (line_text.length > 6) { // More then "\\end{" and "}" characters are neccessary for a usefull accessment begin_index = line_text.lastIndexOf("\\begin{"); end_index = line_text.lastIndexOf("\\end{"); if (begin_index > end_index) { let environment = line_text.substring(begin_index + 7, line_text.indexOf("}", begin_index + 7)); if (this.environment_type_dict.hasOwnProperty(environment)) { return this.environment_type_dict[environment]; // environment beginning found -> return environment type } } else if (end_index > begin_index) { let environment = line_text.substring(end_index + 5, line_text.indexOf("}", end_index + 5)); if (this.environment_type_dict.hasOwnProperty(environment)) { return "text"; // environment ending found -> in text area } } else { break; } line_text = line_text.substring(0, begin_index > end_index ? begin_index : end_index); } } return; // nothing found, return to default behaviour } private filterParameters(symbol_name: string, parameter_match: string, position: vscode.Position): vscode.CompletionItem[] { if (this.parameter_dictionary.hasOwnProperty(symbol_name)) { var completionItems = new Array(); fillSymbols(completionItems, this.parameter_dictionary[symbol_name], parameter_match, position); return completionItems; } return []; } private searchAndCreateLableSymbols(document: vscode.TextDocument, symbol_lable: string, position: vscode.Position): vscode.CompletionItem[] { var completionItems = new Array(); for (var i = 0; i < document.lineCount; i++) { var line = document.lineAt(i); if (line.isEmptyOrWhitespace) { continue; } var line_text = line.text; var index_start = line_text.indexOf("\\label{") + 7; // add 7 to considure the length of "\label{" if (index_start > 6) { // index_start returns > -1 iff substring was found, adding 7 => 6 var index_end = line_text.indexOf("}", index_start); if (index_end > -1) { completionItems.push(new vscode.CompletionItem(line_text.substring(index_start, index_end), vscode.CompletionItemKind.Reference)); } } } return completionItems; } private filterSymbols(symbolsCollection: vscode.CompletionItem[], match: string, position: vscode.Position): vscode.CompletionItem[] { if (!match || match.length === 0) { // no filtering neccessary (empty string is substring of all lables) -> return all return symbolsCollection; } var filtert_symbols = new Array(); var range = new vscode.Range(new vscode.Position(position.line, position.character - match.length), position); for (var i = 0; i < symbolsCollection.length; i++) { var item = symbolsCollection[i]; var clonedItem; if (item.label.includes(match)) { clonedItem = new vscode.CompletionItem(item.label, item.kind); if (item.documentation) { clonedItem.documentation = item.documentation; } if (item.insertText) { clonedItem.insertText = item.insertText; } if (range) { clonedItem.range = range; } clonedItem.filterText = match; clonedItem.sortText = item.label.indexOf(match) + item.label; filtert_symbols.push(clonedItem); } } return filtert_symbols; } private getFileNames(path: string, resolve: (completionItems: vscode.CompletionItem[]) => void, reject: (message: string) => void): void { child_process.exec("ls " + path + " -t", (error: Error, stdout: string, stderr: string) => { var fileNames = stdout.split("\n"); var completionItems = new Array(); for (var i = 0; i < fileNames.length; i++) { completionItems.push(new vscode.CompletionItem(fileNames[i], vscode.CompletionItemKind.File)); } resolve(completionItems); }); } provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable { return new Promise((resolve, reject) => { var line = document.lineAt(position); if (line.isEmptyOrWhitespace) { return resolve([]); } var line_text = line.text; var line_before_pos = line_text.substring(0, position.character).trim(); var start_index, end_index: number; var symbol_lable, symbol_parameter: string; if ((start_index = line_before_pos.lastIndexOf("\\") + 1) > 0) { if ((end_index = line_before_pos.indexOf("{", start_index)) > -1) { symbol_lable = line_before_pos.substring(start_index, end_index); symbol_parameter = line_before_pos.substring(end_index + 1); if ((end_index = symbol_lable.indexOf("[")) > -1) { // check for optional symbol parameters and ignore them symbol_lable = symbol_lable.substring(0, end_index); } switch (symbol_lable) { case "begin": case "end": return resolve(this.filterSymbols(this.environment_symbols, symbol_parameter, position)); case "ref": case "eqref": case "pageref": return resolve(this.searchAndCreateLableSymbols(document, symbol_parameter, position)); case "usepackage": // provide package symbols by first all usepackage parameters before the last "," return resolve(this.filterSymbols(this.package_symbols, symbol_parameter.substring(symbol_parameter.lastIndexOf(",") + 1).trim(), position)); case "documentclass": return resolve(this.filterSymbols(this.documentclass_symbols, symbol_parameter, position)); case "input": case "include": case "includegraphics": case "lstinputlisting": return this.getFileNames(symbol_parameter, resolve, reject); // runs async -> let getFileNames resolve / reject the promise default: // default to NO suggestions return resolve([]); } } else if ((end_index = line_before_pos.indexOf("[", start_index)) > -1) { symbol_lable = line_before_pos.substring(start_index, end_index); symbol_parameter = line_before_pos.substring(end_index + 1) return resolve(this.filterParameters(symbol_lable, symbol_parameter.substring(symbol_parameter.lastIndexOf(",") + 1).trim(), position)); } else { symbol_lable = line_before_pos.substring(start_index); switch (this.getEnvironmentType(document, line_before_pos, position)) { case "math": return resolve(this.filterSymbols(this.math_symbols, symbol_lable, position)); case "tikz": return resolve(this.filterSymbols(this.tikz_symbols, symbol_lable, position)); case "text": return resolve(this.filterSymbols(this.text_symbols, symbol_lable, position)); default: return resolve(this.filterSymbols(this.preamble_symbols, symbol_lable, position)); } } } resolve([]); }).then((res) => res); } }