VSCode_LaTeX/src/completionItemProvider.ts

300 lines
14 KiB
TypeScript

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<vscode.CompletionItem>();
this.text_symbols = new Array<vscode.CompletionItem>();
this.math_symbols = new Array<vscode.CompletionItem>();
this.package_symbols = new Array<vscode.CompletionItem>();
this.environment_symbols = new Array<vscode.CompletionItem>();
this.documentclass_symbols = new Array<vscode.CompletionItem>();
this.tikz_symbols = new Array<vscode.CompletionItem>();
// 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<vscode.CompletionItem>();
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<vscode.CompletionItem>();
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<vscode.CompletionItem>();
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<vscode.CompletionItem>();
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<vscode.CompletionItem[]> {
return new Promise<vscode.CompletionItem[]>((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);
}
}