Skip to content

Commit c209ea3

Browse files
authored
Merge pull request #448 from codefori/feature/function_suggestions
Support for signature information, built-in function improvements
2 parents 6feca9c + ad7332f commit c209ea3

File tree

9 files changed

+1028
-858
lines changed

9 files changed

+1028
-858
lines changed

extension/server/src/providers/apis/bif.ts

Lines changed: 681 additions & 0 deletions
Large diffs are not rendered by default.

extension/server/src/providers/completionItem.ts

Lines changed: 126 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import path = require('path');
2-
import { CompletionItem, CompletionItemKind, CompletionParams, InsertTextFormat, Position, Range } from 'vscode-languageserver';
2+
import { CompletionItem, CompletionItemKind, CompletionParams, InsertTextFormat, InsertTextMode, Position, Range, TextEdit } from 'vscode-languageserver';
33
import { documents, getWordRangeAtPosition, parser, prettyKeywords } from '.';
4-
import Cache from '../../../../language/models/cache';
4+
import Cache, { RpgleType, RpgleVariableType } from '../../../../language/models/cache';
55
import Declaration from '../../../../language/models/declaration';
66
import * as ileExports from './apis';
77
import skipRules from './linter/skipRules';
88
import * as Project from "./project";
99
import { getInterfaces } from './project/exportInterfaces';
1010
import Parser from '../../../../language/parser';
1111
import { Token } from '../../../../language/types';
12+
import { getBuiltIn, getBuiltIns, getBuiltInsForType } from './apis/bif';
1213

1314
const completionKind = {
1415
function: CompletionItemKind.Interface,
@@ -17,6 +18,19 @@ const completionKind = {
1718

1819
const eol = `\n`;
1920

21+
const builtInFunctionCompletionItems: CompletionItem[] = getBuiltIns().map(builtIn => {
22+
const item = CompletionItem.create(builtIn.name);
23+
item.filterText = builtIn.name.substring(1);
24+
item.kind = CompletionItemKind.Function;
25+
item.detail = builtIn.returnType || `void`;
26+
item.documentation = `Built-in function`;
27+
item.insertText = builtIn.name + `(\${1})`;
28+
item.insertTextFormat = InsertTextFormat.Snippet;
29+
item.command = {command: `editor.action.triggerParameterHints`, title: `Trigger Parameter Hints`};
30+
item.sortText = item.filterText;
31+
return item;
32+
});
33+
2034
export default async function completionItemProvider(handler: CompletionParams): Promise<CompletionItem[]> {
2135
const items: CompletionItem[] = [];
2236
const lineNumber = handler.position.line;
@@ -30,38 +44,35 @@ export default async function completionItemProvider(handler: CompletionParams):
3044
if (doc) {
3145
const isFree = (document.getText(Range.create(0, 0, 0, 6)).toUpperCase() === `**FREE`);
3246

33-
// If they're typing inside of a procedure, let's get the stuff from there too
34-
const currentProcedure = doc.procedures.find((proc, index) =>
35-
proc.range.start && proc.range.end &&
36-
lineNumber >= proc.range.start &&
37-
(lineNumber <= proc.range.end + 1 || index === doc.procedures.length - 1) &&
38-
currentPath === proc.position.path
39-
);
40-
4147
const currentLine = document.getText(Range.create(
4248
handler.position.line,
4349
0,
4450
handler.position.line,
4551
200
4652
));
4753

48-
// This means we're just looking for subfields in the struct
4954
if (trigger === `.`) {
55+
56+
//================================================
57+
// Logic for showing subfields (subitems) on symbols
58+
//================================================
59+
5060
const cursorIndex = handler.position.character;
5161
let tokens = Parser.lineTokens(isFree ? currentLine : currentLine.length >= 7 ? ``.padEnd(7) + currentLine.substring(7) : ``, 0, 0, true);
5262

5363
if (tokens.length > 0) {
5464

5565
// We need to find the innermost block we are part of
56-
tokens = Parser.fromBlocksGetTokens(tokens, cursorIndex);
66+
tokens = Parser.fromBlocksGetTokens(tokens, cursorIndex).block;
5767

5868
// Remove any tokens after the cursor
5969
tokens = tokens.filter(token => token.range.end <= cursorIndex);
6070

6171
// Get the possible variable we're referring to
62-
let tokenIndex = Parser.getReference(tokens, cursorIndex);
72+
const referenceStart = Parser.getReference(tokens, cursorIndex);
73+
let tokenIndex = referenceStart;
6374

64-
let currentDef: Declaration | undefined;
75+
let currentDef: Declaration|undefined;
6576

6677
for (tokenIndex; tokenIndex < tokens.length; tokenIndex++) {
6778
if (tokens[tokenIndex] === undefined || [`block`, `dot`, `newline`].includes(tokens[tokenIndex].type)) {
@@ -73,39 +84,91 @@ export default async function completionItemProvider(handler: CompletionParams):
7384
if (!word) break;
7485

7586
if (currentDef) {
76-
if (currentDef.subItems && currentDef.subItems.length > 0) {
87+
if (currentDef.type !== `procedure` && currentDef.subItems && currentDef.subItems.length > 0) {
7788
currentDef = currentDef.subItems.find(subItem => subItem.name.toUpperCase() === word);
7889
}
7990

8091
} else {
81-
currentDef = [
82-
// First we search the local procedure
83-
currentProcedure && currentProcedure.scope ? currentProcedure.scope.parameters.find(parm => parm.name.toUpperCase() === word && parm.subItems.length > 0) : undefined,
84-
currentProcedure && currentProcedure.scope ? currentProcedure.scope.structs.find(struct => struct.name.toUpperCase() === word && struct.keyword[`QUALIFIED`]) : undefined,
85-
currentProcedure && currentProcedure.scope ? currentProcedure.scope.constants.find(struct => struct.subItems.length > 0 && struct.name.toUpperCase() === word) : undefined,
86-
87-
// Then we search the globals
88-
doc.structs.find(struct => struct.name.toUpperCase() === word && struct.keyword[`QUALIFIED`]),
89-
doc.constants.find(constants => constants.subItems.length > 0 && constants.name.toUpperCase() === word)
90-
].find(x => x); // find the first non-undefined item
91-
92-
if (currentDef && currentDef.subItems.length > 0) {
92+
currentDef = doc.findDefinition(lineNumber, word);
93+
94+
if (currentDef) {
95+
if (currentDef.type === `struct` && currentDef.keyword[`QUALIFIED`] === undefined) {
96+
currentDef = undefined;
97+
}
98+
9399
// All good!
94100
} else {
95101
currentDef = undefined;
96102
}
97103
}
98104
}
99105

100-
if (currentDef && currentDef.subItems.length > 0) {
101-
items.push(...currentDef.subItems.map(subItem => {
102-
const item = CompletionItem.create(subItem.name);
103-
item.kind = CompletionItemKind.Property;
104-
item.insertText = subItem.name;
105-
item.detail = prettyKeywords(subItem.keyword);
106-
item.documentation = subItem.description + ` (${currentDef.name})`;
107-
return item;
108-
}));
106+
let onType: RpgleType|undefined;
107+
let onArray = false;
108+
109+
if (currentDef) {
110+
if (currentDef.subItems.length > 0) {
111+
items.push(...currentDef.subItems.map(subItem => {
112+
const item = CompletionItem.create(subItem.name);
113+
item.kind = CompletionItemKind.Property;
114+
item.insertText = subItem.name;
115+
item.detail = prettyKeywords(subItem.keyword);
116+
item.documentation = subItem.description + ` (${currentDef.name})`;
117+
return item;
118+
}));
119+
}
120+
121+
const typeDetail = doc.resolveType(currentDef);
122+
if (typeDetail.type) {
123+
onType = typeDetail.type.name;
124+
onArray = typeDetail.type.isArray;
125+
}
126+
} else if (tokens[referenceStart].type === `builtin` && tokens[referenceStart].value) {
127+
const builtIn = getBuiltIn(tokens[referenceStart].value);
128+
if (builtIn) {
129+
onType = builtIn.returnType;
130+
}
131+
}
132+
133+
//================================================
134+
// How about showing applicable built-in functions for a given type?
135+
//================================================
136+
137+
if (onType) {
138+
const usableFunctions = getBuiltInsForType(onType, onArray);
139+
if (usableFunctions.length > 0) {
140+
const changeRange = Range.create(
141+
handler.position.line,
142+
tokens[referenceStart].range.start,
143+
handler.position.line,
144+
tokens[tokens.length-1].range.end
145+
);
146+
147+
const refValue = currentLine.substring(tokens[referenceStart].range.start, tokens[tokens.length-1].range.start);
148+
149+
for (let func of usableFunctions) {
150+
let builtInFunction = CompletionItem.create(func.name.substring(1));
151+
builtInFunction.kind = CompletionItemKind.Function;
152+
153+
const requiredParms = func.parameters.filter(p => !p.optional);
154+
155+
builtInFunction.additionalTextEdits = [TextEdit.del(changeRange)];
156+
builtInFunction.insertText = `${func.name}(` + requiredParms.map((p, i) => {
157+
if (p.base) {
158+
return refValue
159+
} else {
160+
return `\${${i+1}:${p.name}}`
161+
}
162+
}).join(`:`) + `)`;
163+
builtInFunction.insertTextFormat = InsertTextFormat.Snippet;
164+
165+
// To trigger the signature information
166+
builtInFunction.command = {command: `editor.action.triggerParameterHints`, title: `Trigger Parameter Hints`};
167+
168+
builtInFunction.detail = `Built-in function`;
169+
items.push(builtInFunction);
170+
}
171+
}
109172
}
110173
}
111174
} else {
@@ -128,6 +191,11 @@ export default async function completionItemProvider(handler: CompletionParams):
128191
items.push(...skipRules);
129192

130193
} else {
194+
195+
//================================================
196+
// Content assist on existing symbols
197+
//================================================
198+
131199
const expandScope = (localCache: Cache) => {
132200
for (const subItem of localCache.parameters) {
133201
const item = CompletionItem.create(subItem.name);
@@ -149,6 +217,10 @@ export default async function completionItemProvider(handler: CompletionParams):
149217
item.insertText = `${procedure.name}(${procedure.subItems.map((parm, index) => `\${${index + 1}:${parm.name}}`).join(`:`)})`;
150218
item.detail = prettyKeywords(procedure.keyword);
151219
item.documentation = procedure.description;
220+
221+
if (procedure.subItems.length > 0) {
222+
item.command = { command: `editor.action.triggerParameterHints`, title: `Trigger Parameter Hints` };
223+
}
152224
items.push(item);
153225
}
154226

@@ -241,6 +313,14 @@ export default async function completionItemProvider(handler: CompletionParams):
241313

242314
expandScope(doc);
243315

316+
// If they're typing inside of a procedure, let's get the stuff from there too
317+
const currentProcedure = doc.procedures.find((proc, index) =>
318+
proc.range.start && proc.range.end &&
319+
lineNumber >= proc.range.start &&
320+
(lineNumber <= proc.range.end + 1 || index === doc.procedures.length - 1) &&
321+
currentPath === proc.position.path
322+
);
323+
244324
if (currentProcedure) {
245325
// If we have the entire scope, perfect
246326
if (currentProcedure.scope) {
@@ -261,6 +341,10 @@ export default async function completionItemProvider(handler: CompletionParams):
261341
}
262342
}
263343

344+
//================================================
345+
// Auto-import logic
346+
//================================================
347+
264348
if (isFree) {
265349
const isInclude = currentPath.toLowerCase().endsWith(`.rpgleinc`);
266350
const insertAt = doc.getDefinitionBlockEnd(document.uri) + 1;
@@ -317,6 +401,12 @@ export default async function completionItemProvider(handler: CompletionParams):
317401
items.push(item);
318402
})
319403
}
404+
405+
//================================================
406+
// Showing the available built-in functions
407+
//================================================
408+
409+
items.push(...builtInFunctionCompletionItems);
320410
}
321411
}
322412
}

extension/server/src/providers/hover.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Hover, HoverParams, MarkupKind, Range } from 'vscode-languageserver';
2-
import { documents, getWordRangeAtPosition, parser, prettyKeywords } from '.';
2+
import { documents, getReturnValue, getWordRangeAtPosition, parser, prettyKeywords } from '.';
33
import Parser from "../../../../language/parser";
44
import { URI } from 'vscode-uri';
55
import { Keywords } from '../../../../language/parserTypes';
@@ -30,14 +30,7 @@ export default async function hoverProvider(params: HoverParams): Promise<Hover
3030
}
3131

3232
let markdown = ``;
33-
let returnValue = `void`
34-
35-
let returnKeywords: Keywords = {
36-
...symbol.keyword,
37-
};
38-
delete returnKeywords[`EXTPROC`];
39-
40-
if (Object.keys(returnKeywords).length > 0) returnValue = prettyKeywords(returnKeywords);
33+
const returnValue = getReturnValue(symbol);
4134

4235
const returnTag = symbol.tags.find(tag => tag.tag === `return`);
4336
const deprecatedTag = symbol.tags.find(tag => tag.tag === `deprecated`);

extension/server/src/providers/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
TextDocument
1111
} from 'vscode-languageserver-textdocument';
1212
import Parser from '../../../../language/parser';
13+
import Declaration from '../../../../language/models/declaration';
1314

1415
type Keywords = { [key: string]: string | boolean };
1516

@@ -60,4 +61,16 @@ export function prettyKeywords(keywords: Keywords, filter: boolean = false): str
6061
return undefined;
6162
}
6263
}).filter(k => k).join(` `);
64+
}
65+
66+
export function getReturnValue(symbol: Declaration): string {
67+
let returnValue = `void`
68+
69+
let returnKeywords: Keywords = {
70+
...symbol.keyword,
71+
};
72+
delete returnKeywords[`EXTPROC`];
73+
74+
if (Object.keys(returnKeywords).length > 0) returnValue = prettyKeywords(returnKeywords);
75+
return returnValue;
6376
}

0 commit comments

Comments
 (0)