Implement completion.

This commit is contained in:
CismonX 2020-10-27 03:37:05 +08:00
parent 319d1df24e
commit b1b72e453d
Signed by: cismonx
GPG Key ID: 3094873E29A482FB
4 changed files with 167 additions and 81 deletions

View File

@ -112,6 +112,16 @@
"default": "makeinfo", "default": "makeinfo",
"markdownDescription": "Path to the `makeinfo` (or `texi2any`) command. If not located in `$PATH`, an absolute path should be specified.\n\nThe value should not contain any command-line arguments, just the filename." "markdownDescription": "Path to the `makeinfo` (or `texi2any`) command. If not located in `$PATH`, an absolute path should be specified.\n\nThe value should not contain any command-line arguments, just the filename."
}, },
"texinfo.completion.enableSnippets": {
"type": "boolean",
"default": true,
"markdownDescription": "Show snippets in code completion items.\n\nIf disabled, only commands are shown."
},
"texinfo.completion.hideSnippetCommands": {
"type": "boolean",
"default": true,
"markdownDescription": "When snippet is enabled, hide the snippets' corresponding commands from completion items."
},
"texinfo.preview.noHeaders": { "texinfo.preview.noHeaders": {
"type": "boolean", "type": "boolean",
"default": false, "default": false,

View File

@ -24,6 +24,14 @@ export default class Options implements vscode.Disposable {
return Options.instance.getString('makeinfo'); return Options.instance.getString('makeinfo');
} }
static get enableSnippets() {
return Options.instance.getBoolean('completion.enableSnippets');
}
static get hideSnippetCommands() {
return Options.instance.getBoolean('completion.hideSnippetCommands');
}
static get noHeaders() { static get noHeaders() {
return Options.instance.getBoolean('preview.noHeaders'); return Options.instance.getBoolean('preview.noHeaders');
} }

View File

@ -6,87 +6,133 @@
*/ */
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import Options from '../options';
import { CompletionItem } from '../utils/types';
/** /**
* Provide code completion info for Texinfo documents. * Provide code completion info for Texinfo documents.
*/ */
export default class CompletionItemProvider implements vscode.CompletionItemProvider { export default class CompletionItemProvider implements vscode.CompletionItemProvider {
private completionItems?: CompletionItem[];
/** /**
* Full list of completion items. * Full list of completion items.
* *
* Excerpted from the {@link https://www.gnu.org/software/texinfo/manual/texinfo GNU Texinfo manual}, * Excerpted from the {@link https://www.gnu.org/software/texinfo/manual/texinfo GNU Texinfo manual},
* which is licensed under the GNU Free Documentation License. * which is licensed under the GNU Free Documentation License.
*/ */
private readonly completionItems = <vscode.CompletionItem[]> [ private get values() {
command('ampchar', 'Insert an ampersand, "&"', { hasEmptyArguments: true }), const enableSnippets = Options.enableSnippets;
command('atchar', 'Insert an at sign, "@"', { hasEmptyArguments: true }), const hideSnippetCommands = Options.hideSnippetCommands;
command('backslashchar', 'Insert a blackslash, "\\"', { hasEmptyArguments: true }), return this.completionItems ??= [
command('lbracechar', 'Insert a left brace, "{"', { hasEmptyArguments: true }), command('ampchar', 'Insert an ampersand, "&"', { hasEmptyArguments: true }),
command('rbracechar', 'Insert a right brace, "{"', { hasEmptyArguments: true }), command('atchar', 'Insert an at sign, "@"', { hasEmptyArguments: true }),
command('AA', 'Generate the uppercase Scandinavian A-ring letter, "Å"', { hasEmptyArguments: true }), command('backslashchar', 'Insert a blackslash, "\\"', { hasEmptyArguments: true }),
command('aa', 'Generate the lowercase Scandinavian A-ring letter, "å"', { hasEmptyArguments: true }), command('lbracechar', 'Insert a left brace, "{"', { hasEmptyArguments: true }),
...braceCommand('abbr', 'Indicate a general abbreviation', 1, 'abbreviation', 'meaning'), command('rbracechar', 'Insert a right brace, "{"', { hasEmptyArguments: true }),
...braceCommand('acronym', 'Indicate an acronym in all capital letters', 1, 'acronym', 'meaning'), command('AA', 'Generate the uppercase Scandinavian A-ring letter, "Å"', { hasEmptyArguments: true }),
command('AE', 'Generate the uppercase AE ligatures, "Æ"', { hasEmptyArguments: true }), command('aa', 'Generate the lowercase Scandinavian A-ring letter, "å"', { hasEmptyArguments: true }),
command('ae', 'Generate the lowercase AE ligatures, "æ"', { hasEmptyArguments: true }), ...braceCommand('abbr', 'Indicate a general abbreviation', 1, 'abbreviation', 'meaning'),
command('afivepaper', 'Change page dimensions for the A5 paper size'), ...braceCommand('acronym', 'Indicate an acronym in all capital letters', 1, 'acronym', 'meaning'),
command('afourlatex', 'Change page dimensions for the A4 paper size'), command('AE', 'Generate the uppercase AE ligatures, "Æ"', { hasEmptyArguments: true }),
command('afourpaper', 'Change page dimensions for the A4 paper size'), command('ae', 'Generate the lowercase AE ligatures, "æ"', { hasEmptyArguments: true }),
command('afourwide', 'Change page dimensions for the A4 paper size'), command('afivepaper', 'Change page dimensions for the A5 paper size'),
snippet('alias', 'alias', 'Defines a new command to be just like an existing one', 0, '@alias new=existing', command('afourlatex', 'Change page dimensions for the A4 paper size'),
'alias ${1:new}=${2:existing}'), command('afourpaper', 'Change page dimensions for the A4 paper size'),
command('alias', 'Defines a new command to be just like an existing one', { sortOrder: 1 }), command('afourwide', 'Change page dimensions for the A4 paper size'),
...lineCommandEnum('allowcodebreaks', 'Control breaking at "-" and "_" in TeX', 'true', 'false'), snippet('alias', 'alias', 'Defines a new command to be just like an existing one', 0,
...braceCommand('anchor', 'Define current location for use as a cross-reference target', 1, 'name'), '@alias new=existing', 'alias ${1:new}=${2:existing}'),
...lineCommand('appendix', 'Begin an appendix', 'title'), command('alias', 'Defines a new command to be just like an existing one', { snippet: true }),
...lineCommand('appendixsec', 'Begin an appendix section within an appendix', 'title'), ...lineCommandEnum('allowcodebreaks', 'Control breaking at "-" and "_" in TeX', 'true', 'false'),
...lineCommand('appendixsection', 'Begin an appendix section within an appendix', 'title'), ...braceCommand('anchor', 'Define current location for use as a cross-reference target', 1, 'name'),
...lineCommand('appendixsubsec', 'Begin an appendix subsection', 'title'), ...lineCommand('appendix', 'Begin an appendix', 'title'),
...lineCommand('appendixsubsubsec', 'Begin an appendix subsubsection', 'title'), ...lineCommand('appendixsec', 'Begin an appendix section within an appendix', 'title'),
command('arrow', 'Generate a right arrow glyph, "→"', { hasEmptyArguments: true }), ...lineCommand('appendixsection', 'Begin an appendix section within an appendix', 'title'),
command('asis', 'Print the tables first column without highlighting'), ...lineCommand('appendixsubsec', 'Begin an appendix subsection', 'title'),
...lineCommand('author', 'Set the names of the author(s)', 'author-name'), ...lineCommand('appendixsubsubsec', 'Begin an appendix subsubsection', 'title'),
...braceCommand('b', 'Set text in a bold font', 1, 'text'), command('arrow', 'Generate a right arrow glyph, "→"', { hasEmptyArguments: true }),
...blockCommand('copying', 'Declare copying permissions'), command('asis', 'Print the tables first column without highlighting'),
command('bullet', 'Generate a large round dot, "•"', { hasEmptyArguments: true }), ...lineCommand('author', 'Set the names of the author(s)', 'author-name'),
command('bye', 'stop formatting'), ...braceCommand('b', 'Set text in a bold font', 1, 'text'),
...lineCommand('c', 'Begin a line comment', 'comment'), ...blockCommand('copying', 'Declare copying permissions'),
snippet('header', 'c', 'Declare header block', 2, '@c %**start of header\n\n@c %**end of header', command('bullet', 'Generate a large round dot, "•"', { hasEmptyArguments: true }),
'c %**${1:start of header}\n$3\n@c %**${2:end of header}'), command('bye', 'Stop formatting'),
...braceCommand('caption', 'Define the full caption for a @float', 1, 'definition'), ...lineCommand('c', 'Begin a line comment', 'comment'),
...blockCommand('cartouche', 'Highlight by drawing a box with rounded corners around it'), snippet('header', 'c', 'Declare header block', 2, '@c %**start of header\n\n@c %**end of header',
...lineCommand('center', 'Center the line of text following the command', 'text-line'), 'c %**${1:start of header}\n$3\n@c %**${2:end of header}'),
...lineCommand('centerchap', 'Like @chapter, but centers the chapter title', 'text-line'), ...braceCommand('caption', 'Define the full caption for a @float', 1, 'definition'),
...lineCommand('chapheading', 'Print an unnumbered chapter-like heading', 'title'), ...blockCommand('cartouche', 'Highlight by drawing a box with rounded corners around it'),
...lineCommand('chapter', 'Begin a numbered chapter', 'title'), ...lineCommand('center', 'Center the line of text following the command', 'text-line'),
...lineCommand('cindex', 'Add entry to the index of concepts', 'entry'), ...lineCommand('centerchap', 'Like @chapter, but centers the chapter title', 'text-line'),
...braceCommand('cite', 'Highlight the name of a reference', 1, 'reference'), ...lineCommand('chapheading', 'Print an unnumbered chapter-like heading', 'title'),
...lineCommand('clear', 'Unset flag', 'flag'), ...lineCommand('chapter', 'Begin a numbered chapter', 'title'),
command('click', 'Represent a single "click" in a GUI', { hasEmptyArguments: true }), ...lineCommand('cindex', 'Add entry to the index of concepts', 'entry'),
...braceCommand('clicksequence', 'Represent a sequence of clicks in a GUI', 1, 'actions'), ...braceCommand('cite', 'Highlight the name of a reference', 1, 'reference'),
...lineCommand('clickstyle', 'Execute command on each @click', '@command'), ...lineCommand('clear', 'Unset flag', 'flag'),
...braceCommand('code', 'Indicate text which is a piece of code', 0, 'sample-code'), command('click', 'Represent a single "click" in a GUI', { hasEmptyArguments: true }),
...lineCommandEnum('codequotebacktick', 'Control output of "`" in code examples', 'on', 'off'), ...braceCommand('clicksequence', 'Represent a sequence of clicks in a GUI', 1, 'actions'),
...lineCommandEnum('codequoteundirected', 'Control output of "\'" in code examples', 'on', 'off'), ...lineCommand('clickstyle', 'Execute command on each @click', '@command'),
command('comma', 'Insert a comma character, ","', { hasEmptyArguments: true }), ...braceCommand('code', 'Indicate text which is a piece of code', 0, 'sample-code'),
...braceCommand('command', 'Indicate a command name', 1, 'command-name'), ...lineCommandEnum('codequotebacktick', 'Control output of "`" in code examples', 'on', 'off'),
...lineCommand('comment', 'Begin a line comment', 'comment'), ...lineCommandEnum('codequoteundirected', 'Control output of "\'" in code examples', 'on', 'off'),
command('contents', "Print a complete table of contents."), command('comma', 'Insert a comma character, ","', { hasEmptyArguments: true }),
...blockCommand('copying', 'Specify copyright holders and copying conditions'), ...braceCommand('command', 'Indicate a command name', 1, 'command-name'),
command('copyright', 'The copyright symbol, "©"', { hasEmptyArguments: true }), ...lineCommand('comment', 'Begin a line comment', 'comment'),
...lineCommand('defcodeindex', 'Define a new index, print entries in an @code font', 'index-name'), command('contents', "Print a complete table of contents."),
...lineCommand('defcv', 'Format a description for a variable associated with a class', ...blockCommand('copying', 'Specify copyright holders and copying conditions'),
'category', 'class', 'name'), command('copyright', 'The copyright symbol, "©"', { hasEmptyArguments: true }),
...lineCommand('defcvx', 'Format a description for a variable associated with a class', ...lineCommand('defcodeindex', 'Define a new index, print entries in an @code font', 'index-name'),
'category', 'class', 'name'), ...lineCommandX('defcv', 'Format a description for a variable associated with a class',
...lineCommand('deffn', 'Format a description for a function', 'category', 'name', 'arguments'), 'category', 'class', 'name'),
...lineCommand('deffnx', 'Format a description for a function', 'category', 'name', 'arguments'), ...lineCommandX('deffn', 'Format a description for a function', 'category', 'name', 'arguments'),
...lineCommand('setfilename', 'Provide a name for the output files', 'info-file-name'), ...lineCommand('defindex', 'Define a new index, print entries in a roman font', 'index-name'),
...lineCommand('settitle', 'Specify the title for page headers', 'title'), ...lineCommand('definfoenclose', 'Create a new command for Info that marks text by enclosing it in ' +
command('insertcopying', 'Insert previously defined @copying text'), 'strings that precede and follow the text.', 'newcmd', 'before', 'after'),
...blockCommand('titlepage', 'Declare title page'), ...lineCommandX('defivar', 'Format a description for an instance variable in object-oriented programming',
]; 'class', 'instance-variable-name'),
...lineCommandX('defmac', 'Format a description for a macro', 'macroname', 'arguments'),
...lineCommandX('defmethod', 'Format a description for a method in object-oriented programming',
'class', 'method-name', 'arguments'),
...lineCommandX('defop', 'Format a description for an operation in object-oriented programming',
'category', 'class', 'name', 'arguments'),
...lineCommandX('defopt', 'Format a description for a user option', 'option-name'),
...lineCommandX('defspec', 'Format a description for a special form', 'special-form-name', 'arguments'),
...lineCommandX('deftp', 'Format a description for a data type', 'category', 'name-of-type', 'attributes'),
...lineCommandX('deftypecv', 'Format a description for a typed class variable in ' +
'object-oriented programming', 'category', 'class', 'data-type', 'name'),
...lineCommandX('deftypefn', 'Format a description for a function or similar entity that may ' +
'take arguments and that is typed', 'category', 'data-type', 'name', 'arguments'),
...lineCommandEnum('deftypefnnewline', 'Specifies whether return types for @deftypefn and similar ' +
'are printed on lines by themselves', 'on', 'off'),
...lineCommandX('deftypefun', 'Format a description for a function in a typed language',
'data-type', 'function-name', 'arguments'),
...lineCommandX('deftypeivar', 'Format a description for a typed instance variable in ' +
'object-oriented programming', 'class', 'data-type', 'variable-name'),
...lineCommandX('deftypemethod', 'Format a description for a typed method in object-oriented programming',
'class', 'data-type', 'method-name', 'arguments'),
...lineCommandX('deftypeop', 'Format a description for a typed operation in object-oriented programming',
'category', 'class', 'data-type', 'name', 'arguments'),
...lineCommandX('deftypevar', 'Format a description for a variable in a typed language',
'data-type', 'variable-name'),
...lineCommandX('deftypevr', 'Format a description for something like a variable in a typed language',
'category', 'data-type', 'name'),
...lineCommandX('defun', 'Format a description for a function', 'function-name', 'arguments'),
...lineCommandX('defvar', 'Format a description for a variable', 'variable-name'),
...lineCommandX('defvr', 'Format a description for any kind of variable', 'category', 'name'),
command('detailmenu', 'Mark the (optional) detailed node listing in a master menu'),
...lineCommand('setfilename', 'Provide a name for the output files', 'info-file-name'),
...lineCommand('settitle', 'Specify the title for page headers', 'title'),
command('insertcopying', 'Insert previously defined @copying text'),
...blockCommand('titlepage', 'Declare title page'),
].filter(completionItem => {
if (!enableSnippets) return completionItem.kind === vscode.CompletionItemKind.Function;
return !hideSnippetCommands || !completionItem.snippet;
});
}
private oldOptions?: Options;
provideCompletionItems( provideCompletionItems(
document: vscode.TextDocument, document: vscode.TextDocument,
@ -94,6 +140,9 @@ export default class CompletionItemProvider implements vscode.CompletionItemProv
token: vscode.CancellationToken, token: vscode.CancellationToken,
context: vscode.CompletionContext, context: vscode.CompletionContext,
) { ) {
const lineText = document.lineAt(position.line).text;
// Ignore comment line.
if (lineText.startsWith('@c ') || lineText.startsWith('@comment ')) return undefined;
// Triggered in the middle of a word. // Triggered in the middle of a word.
if (context.triggerKind === vscode.CompletionTriggerKind.Invoke) { if (context.triggerKind === vscode.CompletionTriggerKind.Invoke) {
const wordRange = document.getWordRangeAtPosition(position); const wordRange = document.getWordRangeAtPosition(position);
@ -102,12 +151,17 @@ export default class CompletionItemProvider implements vscode.CompletionItemProv
position = wordRange.start; position = wordRange.start;
if (document.getText(new vscode.Range(position.translate(0, -1), position)) !== '@') return undefined; if (document.getText(new vscode.Range(position.translate(0, -1), position)) !== '@') return undefined;
} }
if (position.character === 1) return this.completionItems; // Check whether options has changed.
if (this.oldOptions !== Options.instance) {
this.oldOptions = Options.instance;
this.completionItems = undefined;
}
if (position.character === 1) return this.values;
// Check whether the '@' character is escaped. // Check whether the '@' character is escaped.
if (document.getText(new vscode.Range(position.translate(0, -2), position.translate(0, -1))) === '@') { if (document.getText(new vscode.Range(position.translate(0, -2), position.translate(0, -1))) === '@') {
return undefined; return undefined;
} else { } else {
return this.completionItems; return this.values;
} }
} }
} }
@ -121,21 +175,22 @@ export default class CompletionItemProvider implements vscode.CompletionItemProv
*/ */
function command(name: string, detail: string, extraArgs?: { function command(name: string, detail: string, extraArgs?: {
/** /**
* Sort order for this completion item when names collide. * Whether this command has a snippet.
*/ */
sortOrder?: number, snippet?: boolean,
/** /**
* Whether this command takes no arguments and braces are required. * Whether this command takes no arguments and braces are required.
*/ */
hasEmptyArguments?: boolean, hasEmptyArguments?: boolean,
}): vscode.CompletionItem { }): CompletionItem {
return { return {
label: '@' + name, label: '@' + name,
kind: vscode.CompletionItemKind.Function, kind: vscode.CompletionItemKind.Function,
detail: detail, detail: detail,
sortText: name + (extraArgs?.sortOrder?.toString() ?? ''), sortText: name + (extraArgs?.snippet ? '1' : ''),
filterText: name, filterText: name,
insertText: name + (extraArgs?.hasEmptyArguments ? '{}' : ''), insertText: name + (extraArgs?.hasEmptyArguments ? '{}' : ''),
snippet: extraArgs?.snippet,
}; };
} }
@ -146,7 +201,7 @@ function command(name: string, detail: string, extraArgs?: {
* @param detail * @param detail
*/ */
function blockCommand(name: string, detail: string) { function blockCommand(name: string, detail: string) {
return [blockSnippet(name, detail), command(name, detail, { sortOrder: 1 })]; return [blockSnippet(name, detail), command(name, detail, { snippet: true })];
} }
/** /**
@ -156,7 +211,7 @@ function blockCommand(name: string, detail: string) {
* @param detail * @param detail
*/ */
function braceCommand(name: string, detail: string, numArgsRequired: number, ...args: string[]) { function braceCommand(name: string, detail: string, numArgsRequired: number, ...args: string[]) {
return [braceCommandSnippet(name, detail, numArgsRequired, ...args), command(name, detail, { sortOrder: 1 })]; return [braceCommandSnippet(name, detail, numArgsRequired, ...args), command(name, detail, { snippet: true })];
} }
/** /**
@ -167,7 +222,18 @@ function braceCommand(name: string, detail: string, numArgsRequired: number, ...
* @param args * @param args
*/ */
function lineCommand(name: string, detail: string, ...args: string[]) { function lineCommand(name: string, detail: string, ...args: string[]) {
return [lineCommandSnippet(name, detail, ...args), command(name, detail, { sortOrder: 1 })]; return [lineCommandSnippet(name, detail, ...args), command(name, detail, { snippet: true })];
}
/**
* Build the completion items for a line command with arguments which has an x-form.
*
* @param name
* @param detail
* @param args
*/
function lineCommandX(name: string, detail: string, ...args: string[]) {
return [...lineCommand(name, detail, ...args), ...lineCommand(name + 'x', detail, ...args)];
} }
/** /**
@ -179,7 +245,7 @@ function lineCommand(name: string, detail: string, ...args: string[]) {
function lineCommandEnum(name: string, detail: string, ...items: string[]) { function lineCommandEnum(name: string, detail: string, ...items: string[]) {
return [ return [
snippet(name, name, detail, 0, `@${name} ${items.join('/')}`, `${name} \${1|${items.join(',')}|}`), snippet(name, name, detail, 0, `@${name} ${items.join('/')}`, `${name} \${1|${items.join(',')}|}`),
command(name, detail, { sortOrder: 1}), command(name, detail, { snippet: true }),
]; ];
} }
@ -239,11 +305,11 @@ function snippet(
sortOrder: number, sortOrder: number,
documentation: string, documentation: string,
insertText: string, insertText: string,
): vscode.CompletionItem { ): CompletionItem {
return { return {
label: label, label: label,
kind: vscode.CompletionItemKind.Snippet, kind: vscode.CompletionItemKind.Snippet,
detail: detail + ' (snippet)', detail: detail,
documentation: snippetDocumentation(documentation), documentation: snippetDocumentation(documentation),
sortText: keyword + sortOrder.toString(), sortText: keyword + sortOrder.toString(),
filterText: keyword, filterText: keyword,

View File

@ -19,6 +19,8 @@ export type ExecResult = { data?: string, error: string };
export type FoldingRange = vscode.FoldingRange & { name: string, detail: string }; export type FoldingRange = vscode.FoldingRange & { name: string, detail: string };
export type CompletionItem = vscode.CompletionItem & { snippet?: boolean };
export function isDefined<T>(value: Optional<T>): value is T { export function isDefined<T>(value: Optional<T>): value is T {
return value !== undefined; return value !== undefined;
} }