Skip to content

Commit

Permalink
Showing 5 changed files with 248 additions and 47 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
## [2.4.1] - 2023-XX-XX
- Fix issue [#292](https://github.com/intersystems/language-server/issues/292): Add intellisense for instance variable (i%PropertyName) syntax
- Fix issue [#296](https://github.com/intersystems/language-server/issues/296): Code completion doesn't appear when typing dot in a class name at the class level
- Fix issue [#299](https://github.com/intersystems/language-server/issues/299): Add intellisense for class name parameters

## [2.4.0] - 2023-10-17
- Fix issue [#282](https://github.com/intersystems/language-server/issues/282): Syntax error in SQL query using PARTITION or OVER
61 changes: 43 additions & 18 deletions server/src/providers/completion.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CompletionItem, CompletionItemKind, CompletionItemTag, CompletionParams, InsertTextFormat, Position, Range, TextEdit } from 'vscode-languageserver/node';
import { getServerSpec, getLanguageServerSettings, getMacroContext, makeRESTRequest, normalizeSystemName, getImports, findFullRange, getClassMemberContext, quoteUDLIdentifier, documaticHtmlToMarkdown, determineNormalizedPropertyClass, storageKeywordsKeyForToken, getParsedDocument, currentClass } from '../utils/functions';
import { getServerSpec, getLanguageServerSettings, getMacroContext, makeRESTRequest, normalizeSystemName, getImports, findFullRange, getClassMemberContext, quoteUDLIdentifier, documaticHtmlToMarkdown, determineClassNameParameterClass, storageKeywordsKeyForToken, getParsedDocument, currentClass, normalizeClassname } from '../utils/functions';
import { ServerSpec, QueryData, KeywordDoc, MacroContext, compressedline } from '../utils/types';
import { documents, corePropertyParams } from '../utils/variables';
import * as ld from '../utils/languageDefinitions';
@@ -1279,31 +1279,46 @@ export async function onCompletion(params: CompletionParams): Promise<Completion
}
else if (
triggerlang === ld.cls_langindex &&
firsttwotokens.toLowerCase().split(" ")[0] === "property" &&
openparencount > closeparencount &&
(prevline.slice(-2) === ", " || prevline.slice(-1) === "(") &&
prevline.slice(firsttwotokens.length).indexOf("[") === -1
prevline.slice(firsttwotokens.length).indexOf("[") === -1 &&
determineClassNameParameterClass(doc,parsed,params.position.line,thistoken,true) != ""
) {
// This is a Property data type parameter
// This is a class name parameter

// Determine the normalized class name of this Property
const normalizedcls = await determineNormalizedPropertyClass(doc,parsed,params.position.line,server);
// Determine the normalized class name
const normalizedcls = await normalizeClassname(doc,parsed,determineClassNameParameterClass(doc,parsed,params.position.line,thistoken,true),server,params.position.line);
if (normalizedcls === "") {
// If we couldn't determine the class, don't return anything
return null;
}

// Find all parameters that are already used
const existingparams: string[] = [];
for (let i = 5; i < parsed[params.position.line].length; i++) {
const symbolstart: number = parsed[params.position.line][i].p;
const symbolend: number = parsed[params.position.line][i].p + parsed[params.position.line][i].c;
if (params.position.character <= symbolstart) {
break;
}
const symboltext = doc.getText(Range.create(Position.create(params.position.line,symbolstart),Position.create(params.position.line,symbolend)));
if (parsed[params.position.line][i].l == ld.cls_langindex && parsed[params.position.line][i].s == ld.cls_cparam_attrindex) {
existingparams.push(symboltext);
if (prevline.slice(-2) == ", ") {
let openCount = 1;
for (let tkn = thistoken; tkn >= 0; tkn--) {
if (parsed[params.position.line][tkn].l == ld.cls_langindex && parsed[params.position.line][tkn].s == ld.cls_delim_attrindex) {
const delimText = doc.getText(Range.create(
params.position.line,
parsed[params.position.line][tkn].p,
params.position.line,
parsed[params.position.line][tkn].p+parsed[params.position.line][tkn].c
));
if (delimText == ")") {
openCount++;
} else if (delimText == "(") {
openCount--;
if (openCount == 0) break;
}
} else if (parsed[params.position.line][tkn].l == ld.cls_langindex && parsed[params.position.line][tkn].s == ld.cls_cparam_attrindex) {
existingparams.push(doc.getText(Range.create(
params.position.line,
parsed[params.position.line][tkn].p,
params.position.line,
parsed[params.position.line][tkn].p+parsed[params.position.line][tkn].c
)));
}
}
}

@@ -1322,12 +1337,22 @@ export async function onCompletion(params: CompletionParams): Promise<Completion
};
});
result = coreParams.filter(e => !existingparams.includes(e.label));
const isProperty: boolean =
parsed[params.position.line][0].l == ld.cls_langindex &&
parsed[params.position.line][0].s == ld.cls_keyword_attrindex &&
["property","relationship"].includes(doc.getText(Range.create(
params.position.line,
parsed[params.position.line][0].p,
params.position.line,
parsed[params.position.line][0].p+parsed[params.position.line][0].c
)).toLowerCase());

// Query the server to get the names and descriptions of all class-specific parameters
const data: QueryData = {
query: "SELECT Name, Description, Origin, Type, Deprecated FROM %Dictionary.CompiledParameter WHERE parent->ID = ? OR " +
"parent->ID %INLIST (SELECT $LISTFROMSTRING(PropertyClass) FROM %Dictionary.CompiledClass WHERE Name = ?)",
parameters: [normalizedcls,currentClass(doc,parsed)]
query: `SELECT Name, Description, Origin, Type, Deprecated FROM %Dictionary.CompiledParameter WHERE parent->ID = ?${
isProperty ? " OR parent->ID %INLIST (SELECT $LISTFROMSTRING(PropertyClass) FROM %Dictionary.CompiledClass WHERE Name = ?)" : ""
}`,
parameters: isProperty ? [normalizedcls,currentClass(doc,parsed)] : [normalizedcls]
};
const respdata = await makeRESTRequest("POST",1,"/action/query",server,data);
if (respdata !== undefined && "content" in respdata.data.result && respdata.data.result.content.length > 0) {
146 changes: 138 additions & 8 deletions server/src/providers/definition.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Position, TextDocumentPositionParams, Range } from 'vscode-languageserver/node';
import { getServerSpec, findFullRange, normalizeClassname, makeRESTRequest, createDefinitionUri, getMacroContext, isMacroDefinedAbove, quoteUDLIdentifier, getClassMemberContext, determineNormalizedPropertyClass, getParsedDocument, currentClass, getTextForUri } from '../utils/functions';
import { getServerSpec, findFullRange, normalizeClassname, makeRESTRequest, createDefinitionUri, getMacroContext, isMacroDefinedAbove, quoteUDLIdentifier, getClassMemberContext, determineClassNameParameterClass, getParsedDocument, currentClass, getTextForUri } from '../utils/functions';
import { ServerSpec, QueryData } from '../utils/types';
import { documents, corePropertyParams } from '../utils/variables';
import * as ld from '../utils/languageDefinitions';
@@ -1063,7 +1063,7 @@ export async function onDefinition(params: TextDocumentPositionParams) {
}
}
else if (parsed[params.position.line][i].l == ld.cls_langindex && parsed[params.position.line][i].s == ld.cls_cparam_attrindex) {
// This is a Property data type parameter
// This is a class name parameter

// Get the full text of the selection
const paramrange = findFullRange(params.position.line,parsed,i,symbolstart,symbolend);
@@ -1075,12 +1075,12 @@ export async function onDefinition(params: TextDocumentPositionParams) {
return null;
}

// Determine the normalized class name of this Property
const normalizedcls = await determineNormalizedPropertyClass(doc,parsed,params.position.line,server);
if (normalizedcls === "") {
// If we couldn't determine the class, don't return anything
return null;
}
// Verify that is is a parameter for a class name and not a method argument
const clsName = determineClassNameParameterClass(doc,parsed,params.position.line,i);
if (clsName == "") return null;
// Determine the normalized class name
const normalizedcls = await normalizeClassname(doc,parsed,clsName,server,params.position.line);
if (normalizedcls == "") return null;

// If this is a class file, determine what class we're in
const thisclass = doc.languageId === "objectscript-class" ? currentClass(doc,parsed) : "";
@@ -1176,8 +1176,138 @@ export async function onDefinition(params: TextDocumentPositionParams) {
}

// Check if the property is defined in this class
var targetrange = Range.create(Position.create(0,0),Position.create(0,0));
var targetselrange = Range.create(Position.create(0,0),Position.create(0,0));
// Loop through the file contents to find this member
var linect = 0;
for (let dln = 0; dln < parsed.length; dln++) {
if (linect > 0) {
linect++;
if (linect === definitionTargetRangeMaxLines) {
// We've seen the maximum number of lines without hitting the next class member so cut off the preview range here
targetrange.end = Position.create(dln+1,0);
break;
}
if (
parsed[dln].length > 0 && parsed[dln][0].l === ld.cls_langindex &&
(parsed[dln][0].s === ld.cls_keyword_attrindex || parsed[dln][0].s === ld.cls_desc_attrindex)
) {
// This is the first class member following the one we needed the definition for, so cut off the preview range here
targetrange.end = Position.create(dln,0);
break;
}
}
else if (parsed[dln].length > 0 && parsed[dln][0].l == ld.cls_langindex && parsed[dln][0].s == ld.cls_keyword_attrindex) {
// This line starts with a UDL keyword

var keyword = doc.getText(Range.create(Position.create(dln,parsed[dln][0].p),Position.create(dln,parsed[dln][0].p+parsed[dln][0].c))).toLowerCase();
if (keyword.indexOf("property") !== -1 || keyword.indexOf("relationship") !== -1) {
const thismemberrange = findFullRange(dln,parsed,1,parsed[dln][1].p,parsed[dln][1].p+parsed[dln][1].c);
const thismember = doc.getText(thismemberrange);
if (thismember === member) {
// We found the member
targetselrange = thismemberrange;
targetrange.start = Position.create(dln,0);
linect++;
}
}
}
}
if (targetrange.start.line !== 0) {
// Remove any blank lines or comments from the end of the preview range
for (let pvrln = targetrange.end.line-1; pvrln > targetrange.start.line; pvrln--) {
if (parsed[pvrln].length === 0) {
targetrange.end.line = pvrln;
}
else if (parsed[pvrln][0].l === ld.cos_langindex && (parsed[pvrln][0].s === ld.cos_comment_attrindex || parsed[pvrln][0].s === ld.cos_dcom_attrindex)) {
targetrange.end.line = pvrln;
}
else {
break;
}
}
return [{
targetUri: params.textDocument.uri,
originSelectionRange: memberrange,
targetSelectionRange: targetselrange,
targetRange: targetrange
}];
}
// The member is defined in another class

// Query the server to get the origin class of this property
let membernameinfile = member, originclass = "";
const queryrespdata = await makeRESTRequest("POST",1,"/action/query",server,{
query: "SELECT Origin, NULL AS Stub FROM %Dictionary.CompiledProperty WHERE parent->ID = ? AND name = ?",
parameters: [thisclass,unquotedname]
});
if (queryrespdata !== undefined) {
if ("content" in queryrespdata.data.result && queryrespdata.data.result.content.length > 0) {
// We got data back
originclass = queryrespdata.data.result.content[0].Origin;
}
}
if (originclass !== "") {
// Get the uri of the origin class
const newuri = await createDefinitionUri(params.textDocument.uri,originclass,".cls");
if (newuri !== "") {
// Get the full text of the target class
const classText: string[] = await getTextForUri(newuri,server);
if (classText.length) {
// Loop through the file contents to find this member
var linect = 0;
const regex = new RegExp(`^(?:Property|Relationship) ${membernameinfile}(?:\\(|;| )`);
for (let j = 0; j < classText.length; j++) {
if (linect > 0) {
linect++;
if (linect === definitionTargetRangeMaxLines) {
// We've seen the maximum number of lines without hitting the next class member so cut off the preview range here
targetrange.end = Position.create(j+1,0);
break;
}
if (classMemberTypes.indexOf(classText[j].split(" ",1)[0]) !== -1) {
// This is the first class member following the one we needed the definition for, so cut off the preview range here
targetrange.end = Position.create(j,0);
break;
}
}
else if (regex.test(classText[j])) {
// This is the right class member
const memberlineidx = classText[j].indexOf(membernameinfile);
if (memberlineidx !== -1) {
targetselrange = Range.create(Position.create(j,memberlineidx),Position.create(j,memberlineidx+membernameinfile.length));
targetrange.start = Position.create(j,0);
linect++;
}
}
}
if (linect > 0) {
// Remove any blank lines or comments from the end of the preview range
for (let pvrln = targetrange.end.line-1; pvrln > targetrange.start.line; pvrln--) {
const trimmed = classText[pvrln].trim();
if (trimmed === "") {
targetrange.end.line = pvrln;
}
else if (
trimmed.slice(0,3) === "##;" || trimmed.slice(0,2) === "//" || trimmed.slice(0,1) === ";" ||
trimmed.slice(0,2) === "#;" || trimmed.slice(0,2) === "/*"
) {
targetrange.end.line = pvrln;
}
else {
break;
}
}
return [{
targetUri: newuri,
targetRange: targetrange,
originSelectionRange: memberrange,
targetSelectionRange: targetselrange
}];
}
}
}
}

}
break;
12 changes: 8 additions & 4 deletions server/src/providers/hover.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Position, TextDocumentPositionParams, Range } from 'vscode-languageserver/node';
import { getServerSpec, getLanguageServerSettings, findFullRange, normalizeClassname, makeRESTRequest, documaticHtmlToMarkdown, getMacroContext, isMacroDefinedAbove, haltOrHang, quoteUDLIdentifier, getClassMemberContext, beautifyFormalSpec, determineNormalizedPropertyClass, storageKeywordsKeyForToken, getParsedDocument, currentClass } from '../utils/functions';
import { getServerSpec, getLanguageServerSettings, findFullRange, normalizeClassname, makeRESTRequest, documaticHtmlToMarkdown, getMacroContext, isMacroDefinedAbove, haltOrHang, quoteUDLIdentifier, getClassMemberContext, beautifyFormalSpec, determineClassNameParameterClass, storageKeywordsKeyForToken, getParsedDocument, currentClass } from '../utils/functions';
import { ServerSpec, QueryData, CommandDoc, KeywordDoc } from '../utils/types';
import { documents, corePropertyParams } from '../utils/variables';
import * as ld from '../utils/languageDefinitions';
@@ -942,7 +942,11 @@ export async function onHover(params: TextDocumentPositionParams) {
}
}
else if (parsed[params.position.line][i].l == ld.cls_langindex && parsed[params.position.line][i].s == ld.cls_cparam_attrindex) {
// This is a Property data type parameter
// This is a class name parameter

// Verify that is is a parameter for a class name and not a method argument
const clsName = determineClassNameParameterClass(doc,parsed,params.position.line,i);
if (clsName == "") return;

// Get the full text of the selection
const paramrange = findFullRange(params.position.line,parsed,i,symbolstart,symbolend);
@@ -957,8 +961,8 @@ export async function onHover(params: TextDocumentPositionParams) {
};
}

// Determine the normalized class name of this Property
const normalizedcls = await determineNormalizedPropertyClass(doc,parsed,params.position.line,server);
// Determine the normalized class name
const normalizedcls = await normalizeClassname(doc,parsed,clsName,server,params.position.line);
if (normalizedcls !== "") {
const respdata = await makeRESTRequest("POST",1,"/action/query",server,{
query: "SELECT Description, Type FROM %Dictionary.CompiledParameter WHERE Name = ? AND (parent->ID = ? OR " +
75 changes: 58 additions & 17 deletions server/src/utils/functions.ts
Original file line number Diff line number Diff line change
@@ -2008,25 +2008,66 @@ export function documaticHtmlToMarkdown(html: string): string {
}

/**
* Determine the normalized class name of the Property definition starting on `line` of `doc`,
* or the empty string if it can't be determined.
* Used for Property data type parameter intellisense.
* If this class parameter is is a parameter for a class name,
* return the raw name of that class. If the class couldn't be
* determined, or this parameter is a method, query or trigger
* argument, the empty string is returned.
*
* @param doc The TextDocument that the Property definition is in.
* @param parsed The tokenized representation of doc.
* @param line The line that the Property definition starts on.
* @param server The server that doc is associated with.
* @param doc The TextDocument that the class parameter is in.
* @param parsed The tokenized representation of `doc`.
* @param line The line that the class parameter is on.
* @param token The offset of the class parameter in the line.
* @param completion `true` if called from the completion provider.
*/
export async function determineNormalizedPropertyClass(doc: TextDocument, parsed: compressedline[], line: number, server: ServerSpec): Promise<string> {
let firstclstkn = 3;
if (parsed[line][3].l == ld.cls_langindex && parsed[line][3].s == ld.cls_keyword_attrindex) {
// This is a collection Property
firstclstkn = 5;
}
const clsstart = parsed[line][firstclstkn].p;
const clsend = parsed[line][firstclstkn].p+parsed[line][firstclstkn].c;
const clsname = doc.getText(findFullRange(line,parsed,firstclstkn,clsstart,clsend));
return normalizeClassname(doc,parsed,clsname,server,line);
export function determineClassNameParameterClass(doc: TextDocument, parsed: compressedline[], line: number, token: number, completion = false): string {
if (
completion &&
parsed[line][token].l == ld.cls_langindex &&
parsed[line][token].s == ld.error_attrindex &&
["(","()"].includes(doc.getText(Range.create(
line,
parsed[line][token].p,
line,
parsed[line][token].p+parsed[line][token].c
))) && token > 0 &&
parsed[line][token-1].l == ld.cls_langindex &&
parsed[line][token-1].s == ld.cls_clsname_attrindex
) {
// When doing completion for (, the ( may be an
// error token so we need to handle that special case
return doc.getText(findFullRange(
line,parsed,token-1,
parsed[line][token-1].p,
parsed[line][token-1].p+parsed[line][token-1].c
));
}
let openCount = 1, clsName = "";
for (let tkn = token; tkn >= 0; tkn--) {
if (parsed[line][tkn].l == ld.cls_langindex && parsed[line][tkn].s == ld.cls_delim_attrindex) {
const delimText = doc.getText(Range.create(
line,
parsed[line][tkn].p,
line,
parsed[line][tkn].p+parsed[line][tkn].c
));
if (delimText == ")") {
openCount++;
} else if (delimText == "(") {
openCount--;
if (openCount == 0) {
if (tkn > 0 && parsed[line][tkn-1].l == ld.cls_langindex && parsed[line][tkn-1].s == ld.cls_clsname_attrindex) {
clsName = doc.getText(findFullRange(
line,parsed,tkn-1,
parsed[line][tkn-1].p,
parsed[line][tkn-1].p+parsed[line][tkn-1].c
));
}
break;
}
}
}
}
return clsName;
}

/**

0 comments on commit 4bb0fe2

Please sign in to comment.