Skip to content

Commit 0fbc93b

Browse files
authored
Merge pull request #15 from fasfsfgs/signature-helper
Signature helper
2 parents 4b4ccff + 498dbb5 commit 0fbc93b

3 files changed

Lines changed: 293 additions & 61 deletions

File tree

src/cljParser.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
'use strict';
2+
3+
interface ExpressionInfo {
4+
functionName: string;
5+
parameterPosition: number;
6+
}
7+
8+
interface RelativeExpressionInfo {
9+
startPosition: number;
10+
parameterPosition: number;
11+
}
12+
13+
const CLJ_TEXT_DELIMITER = `"`;
14+
const CLJ_TEXT_ESCAPE = `\\`;
15+
const CLJ_COMMENT_DELIMITER = `;`;
16+
export const R_CLJ_WHITE_SPACE = /\s|,/;
17+
const R_CLJ_OPERATOR_DELIMITERS = /\s|,|\(|{|\[/;
18+
19+
/** { close_char open_char } */
20+
const CLJ_EXPRESSION_DELIMITERS: Map<string, string> = new Map<string, string>([
21+
[`}`, `{`],
22+
[`)`, `(`],
23+
[`]`, `[`],
24+
[CLJ_TEXT_DELIMITER, CLJ_TEXT_DELIMITER],
25+
]);
26+
27+
export function getExpressionInfo(text: string): ExpressionInfo {
28+
text = removeCljComments(text);
29+
const relativeExpressionInfo = getRelativeExpressionInfo(text);
30+
if (!relativeExpressionInfo)
31+
return;
32+
33+
let functionName = text.substring(relativeExpressionInfo.startPosition + 1); // expression openning ignored
34+
functionName = functionName.substring(functionName.search(/[^,\s]/)); // trim left
35+
functionName = functionName.substring(0, functionName.search(R_CLJ_OPERATOR_DELIMITERS)); // trim right according to operator delimiter
36+
37+
if (!functionName.length)
38+
return;
39+
40+
return {
41+
functionName,
42+
parameterPosition: relativeExpressionInfo.parameterPosition,
43+
};
44+
}
45+
46+
function removeCljComments(text: string): string {
47+
const lines = text.match(/[^\r\n]+/g); // split string by line
48+
49+
if (lines.length > 1) {
50+
return lines.map(line => removeCljComments(line)).join(`\n`); // remove comments from each line and concat them again after
51+
}
52+
53+
const line = lines[0];
54+
let uncommentedIndex = line.length;
55+
let insideString = false;
56+
for (let i = 0; i < line.length; i++) {
57+
if (line[i] === CLJ_TEXT_DELIMITER) {
58+
insideString = !insideString || line[i - 1] === CLJ_TEXT_ESCAPE;
59+
continue;
60+
}
61+
if (line[i] === CLJ_COMMENT_DELIMITER && !insideString) { // ignore comment delimiter inside a string
62+
uncommentedIndex = i;
63+
break;
64+
}
65+
}
66+
67+
return line.substring(0, uncommentedIndex);
68+
}
69+
70+
function getRelativeExpressionInfo(text: string, openChar: string = `(`): RelativeExpressionInfo {
71+
const relativeExpressionInfo: RelativeExpressionInfo = {
72+
startPosition: text.length - 1,
73+
parameterPosition: -1,
74+
};
75+
76+
let newParameterFound = false;
77+
while (relativeExpressionInfo.startPosition >= 0) {
78+
const char = text[relativeExpressionInfo.startPosition];
79+
80+
// check if found the beginning of the expression (string escape taken care of)
81+
if (char === openChar && (openChar !== CLJ_TEXT_DELIMITER || (text[relativeExpressionInfo.startPosition - 1] !== CLJ_TEXT_ESCAPE))) {
82+
if (newParameterFound) // ignore one parameter found if it's actually the function we're looking for
83+
relativeExpressionInfo.parameterPosition--;
84+
return relativeExpressionInfo;
85+
}
86+
87+
// ignore everything if searching inside a string
88+
if (openChar === CLJ_TEXT_DELIMITER) {
89+
relativeExpressionInfo.startPosition--;
90+
continue;
91+
}
92+
93+
// invalid code if a beginning of an expression is found without being searched for
94+
if (char !== CLJ_TEXT_DELIMITER && containsValue(CLJ_EXPRESSION_DELIMITERS, char))
95+
return;
96+
97+
// keep searching if it's white space
98+
if (R_CLJ_WHITE_SPACE.test(char)) {
99+
if (!newParameterFound) {
100+
relativeExpressionInfo.parameterPosition++;
101+
newParameterFound = true;
102+
}
103+
relativeExpressionInfo.startPosition--;
104+
continue;
105+
}
106+
107+
// check for new expressions
108+
const expressionDelimiter = CLJ_EXPRESSION_DELIMITERS.get(char);
109+
if (!!expressionDelimiter) {
110+
const innerExpressionInfo = getRelativeExpressionInfo(text.substring(0, relativeExpressionInfo.startPosition), expressionDelimiter);
111+
if (!innerExpressionInfo)
112+
return;
113+
114+
relativeExpressionInfo.startPosition = innerExpressionInfo.startPosition - 1;
115+
relativeExpressionInfo.parameterPosition++;
116+
newParameterFound = true;
117+
continue;
118+
}
119+
120+
newParameterFound = false;
121+
relativeExpressionInfo.startPosition--;
122+
}
123+
124+
return; // reached the beginning of the text without finding the start of the expression
125+
}
126+
127+
function containsValue(map: Map<any, any>, checkValue: any): boolean {
128+
for (let value of map.values()) {
129+
if (value === checkValue)
130+
return true;
131+
}
132+
return false;
133+
}

src/clojureMain.ts

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,18 @@
11
'use strict';
22

33
import * as fs from 'fs';
4-
import * as os from 'os';
54
import * as path from 'path';
65
import * as vscode from 'vscode';
76

8-
import {
9-
CLOJURE_MODE
10-
} from './clojureMode';
11-
import {
12-
ClojureCompletionItemProvider
13-
} from './clojureSuggest';
14-
import {
15-
clojureEval
16-
} from './clojureEval';
17-
import {
18-
ClojureDefinitionProvider
19-
} from './clojureDefinition';
20-
import {
21-
ClojureLanguageConfiguration
22-
} from './clojureConfiguration'
23-
import {
24-
ClojureHoverProvider
25-
} from './clojureHover'
26-
import {
27-
nREPLClient
28-
} from './nreplClient';
29-
import {
30-
JarContentProvider
31-
} from './jarContentProvider';
7+
import { CLOJURE_MODE } from './clojureMode';
8+
import { ClojureCompletionItemProvider } from './clojureSuggest';
9+
import { clojureEval } from './clojureEval';
10+
import { ClojureDefinitionProvider } from './clojureDefinition';
11+
import { ClojureLanguageConfiguration } from './clojureConfiguration';
12+
import { ClojureHoverProvider } from './clojureHover';
13+
import { ClojureSignatureProvider } from './clojureSignature';
14+
import { nREPLClient } from './nreplClient';
15+
import { JarContentProvider } from './jarContentProvider';
3216

3317
let connectionIndicator = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
3418

@@ -107,25 +91,26 @@ function connect(context: vscode.ExtensionContext) {
10791

10892
export function activate(context: vscode.ExtensionContext) {
10993
let evaluationResultChannel = vscode.window.createOutputChannel('Evaluation results');
110-
vscode.commands.registerCommand('clojureVSCode.connect', () => {connect(context)});
111-
vscode.commands.registerCommand('clojureVSCode.eval', () => {clojureEval(context)});
112-
vscode.commands.registerCommand('clojureVSCode.evalAndShowResult', () => {clojureEval(context, evaluationResultChannel)});
113-
context.subscriptions.push(vscode.languages.registerCompletionItemProvider(CLOJURE_MODE, new ClojureCompletionItemProvider(context), '.', '/'))
94+
vscode.commands.registerCommand('clojureVSCode.connect', () => { connect(context) });
95+
vscode.commands.registerCommand('clojureVSCode.eval', () => { clojureEval(context) });
96+
vscode.commands.registerCommand('clojureVSCode.evalAndShowResult', () => { clojureEval(context, evaluationResultChannel) });
97+
context.subscriptions.push(vscode.languages.registerCompletionItemProvider(CLOJURE_MODE, new ClojureCompletionItemProvider(context), '.', '/'));
11498
context.subscriptions.push(vscode.languages.registerDefinitionProvider(CLOJURE_MODE, new ClojureDefinitionProvider(context)));
11599
context.subscriptions.push(vscode.languages.registerHoverProvider(CLOJURE_MODE, new ClojureHoverProvider(context)));
100+
context.subscriptions.push(vscode.languages.registerSignatureHelpProvider(CLOJURE_MODE, new ClojureSignatureProvider(context), ' ', '\n'));
116101
vscode.workspace.registerTextDocumentContentProvider('jar', new JarContentProvider());
117102
vscode.languages.setLanguageConfiguration(CLOJURE_MODE.language, new ClojureLanguageConfiguration());
118103

119104
resetConnectionParams(context);
120105
updateConnectionParams(context);
121106
let port = context.workspaceState.get<number>('port');
122107
let host = context.workspaceState.get<string>('host');
123-
if (port && host ) {
108+
if (port && host) {
124109
updateConnectionIndicator(port, host);
125110
testConnection(port, host, (response) => {
126111
vscode.window.showInformationMessage(onSuccesfullConnectMessage)
127112
});
128113
}
129114
}
130115

131-
export function deactivate() {}
116+
export function deactivate() { }

src/clojureSignature.ts

Lines changed: 144 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,153 @@
11
'use strict';
22

3-
import * as fs from 'fs';
4-
import * as os from 'os';
5-
import * as path from 'path';
63
import * as vscode from 'vscode';
7-
import {
8-
nREPLClient
9-
} from './nreplClient';
10-
import {
11-
ClojureProvider
12-
} from './clojureProvider';
4+
5+
import { ClojureProvider } from './clojureProvider';
6+
import * as cljParser from './cljParser';
7+
8+
const PARAMETER_OPEN = `[`;
9+
const PARAMETER_CLOSE = `]`;
10+
const PARAMETER_REST = `&`;
11+
const UNSUPPORTED_SIGNATURE_NAMES = ['.', 'new', 'fn', 'set!']; // they have forms that do not follow the same rules as other special forms
12+
const SPECIAL_FORM_PARAMETER_REST = `*`;
1313

1414
export class ClojureSignatureProvider extends ClojureProvider implements vscode.SignatureHelpProvider {
1515

16-
provideSignatureHelp(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable < vscode.SignatureHelp > {
17-
return new Promise < vscode.SignatureHelp > ((resolve, reject) => {
18-
let wordRange = document.getWordRangeAtPosition(position);
19-
if (wordRange === undefined) {
20-
return reject(); //resolve(new vscode.Hover('Docstring not found'));
21-
}
22-
let currentWord: string;
23-
currentWord = document.lineAt(position.line).text.slice(wordRange.start.character, wordRange.end.character);
24-
let ns = this.getNamespace(document.getText());
25-
26-
let nrepl = this.getNREPL();
27-
nrepl.info(currentWord, ns, (info) => {
28-
if (info.doc) {
29-
let signatureHelp = new vscode.SignatureHelp();
30-
signatureHelp.activeParameter = 0;
31-
signatureHelp.activeSignature = 0;
32-
signatureHelp.signatures = [new vscode.SignatureInformation('a b c')]
33-
resolve(signatureHelp);
34-
}
35-
reject();
16+
provideSignatureHelp(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable<vscode.SignatureHelp> {
17+
const textToGetInfo = document.getText(new vscode.Range(new vscode.Position(0, 0), position));
18+
const exprInfo = cljParser.getExpressionInfo(textToGetInfo);
19+
if (!exprInfo)
20+
return;
21+
22+
const ns = this.getNamespace(document.getText());
23+
24+
return new Promise<vscode.SignatureHelp>((resolve, reject) => {
25+
this.getNREPL().info(exprInfo.functionName, ns, info => {
26+
if (!info.name) // sometimes info brings just a list of suggestions (example: .MAX_VALUE)
27+
return resolve();
28+
29+
if (!!info['special-form'])
30+
return resolve(getSpecialFormSignatureHelp(info, exprInfo.parameterPosition));
31+
32+
return resolve(getFunctionSignatureHelp(info, exprInfo.parameterPosition));
3633
});
3734
});
3835
}
39-
}
36+
37+
}
38+
39+
function getSpecialFormSignatureHelp(info: any, parameterPosition: number): vscode.SignatureHelp {
40+
if (UNSUPPORTED_SIGNATURE_NAMES.indexOf(info.name) > -1) {
41+
const signatureHelp = new vscode.SignatureHelp();
42+
signatureHelp.signatures = [new vscode.SignatureInformation(`${info.name} *special form* ${info['forms-str']}`, info.doc)];
43+
signatureHelp.activeSignature = 0;
44+
45+
return signatureHelp;
46+
}
47+
48+
const forms: string = info['forms-str'];
49+
const [functionName, ...parameters] = forms.substring(3, forms.length - 1).split(' ');
50+
const parameterInfos = parameters.map(parameter => new vscode.ParameterInformation(parameter));
51+
52+
const sigInfo = new vscode.SignatureInformation(`${info.name} *special form* [${parameterInfos.map(pi => pi.label).join(`\n`)}]`, info.doc);
53+
sigInfo.parameters = parameterInfos;
54+
55+
if (parameterPosition + 1 > sigInfo.parameters.length && sigInfo.parameters[sigInfo.parameters.length - 1].label.endsWith(SPECIAL_FORM_PARAMETER_REST))
56+
parameterPosition = sigInfo.parameters.length - 1;
57+
58+
const signatureHelp = new vscode.SignatureHelp();
59+
signatureHelp.signatures = [sigInfo];
60+
signatureHelp.activeParameter = parameterPosition;
61+
signatureHelp.activeSignature = 0;
62+
63+
return signatureHelp;
64+
}
65+
66+
function getFunctionSignatureHelp(info: any, parameterPosition: number): vscode.SignatureHelp {
67+
const signatures = getFunctionSignatureInfos(info);
68+
signatures.sort((sig1, sig2) => sig1.parameters.length - sig2.parameters.length);
69+
70+
let activeSignature = signatures.findIndex(signature => signature.parameters.length >= parameterPosition + 1);
71+
if (activeSignature === -1) {
72+
activeSignature = signatures.findIndex(signature => signature.parameters.some(param => param.label.startsWith(PARAMETER_REST)));
73+
if (activeSignature !== -1)
74+
parameterPosition = signatures[activeSignature].parameters.length - 1;
75+
}
76+
if (activeSignature === -1)
77+
activeSignature = 0;
78+
79+
const signatureHelp = new vscode.SignatureHelp();
80+
signatureHelp.signatures = signatures;
81+
signatureHelp.activeParameter = parameterPosition;
82+
signatureHelp.activeSignature = activeSignature;
83+
84+
return signatureHelp;
85+
}
86+
87+
function getFunctionSignatureInfos(info: any): vscode.SignatureInformation[] {
88+
const arglists: string = info['arglists-str'];
89+
90+
const sigParamStarts: number[] = [];
91+
const sigParamStops: number[] = [];
92+
let nestingLevel = 0;
93+
for (let i = 0; i < arglists.length; i++) {
94+
if (arglists[i] === PARAMETER_OPEN) {
95+
if (nestingLevel === 0)
96+
sigParamStarts.push(i);
97+
nestingLevel++;
98+
}
99+
if (arglists[i] === PARAMETER_CLOSE) {
100+
nestingLevel--;
101+
if (nestingLevel === 0)
102+
sigParamStops.push(i);
103+
}
104+
}
105+
106+
return sigParamStarts
107+
.map((sigParamStart, index) => arglists.substring(sigParamStart, sigParamStops[index] + 1))
108+
.map(signatureParameter => {
109+
const parameterInfos = getFunctionParameterInfos(signatureParameter);
110+
const sigInfo = new vscode.SignatureInformation(`${info.ns}/${info.name} [${parameterInfos.map(pi => pi.label).join(`\n`)}]`);
111+
sigInfo.documentation = info.doc;
112+
sigInfo.parameters = parameterInfos;
113+
return sigInfo;
114+
});
115+
}
116+
117+
function getFunctionParameterInfos(signatureParameter: string): vscode.ParameterInformation[] {
118+
signatureParameter = signatureParameter.substring(1, signatureParameter.length - 1); // removing external brackets
119+
const paramStarts: number[] = [];
120+
const paramStops: number[] = [];
121+
let insideParameter = false;
122+
let bracketsNestingLevel = 0;
123+
for (let i = 0; i < signatureParameter.length; i++) {
124+
const char = signatureParameter[i];
125+
126+
if (!insideParameter) {
127+
insideParameter = true;
128+
paramStarts.push(i);
129+
if (char === PARAMETER_OPEN)
130+
bracketsNestingLevel++;
131+
if (char === PARAMETER_REST)
132+
break;
133+
} else {
134+
if (char === PARAMETER_OPEN)
135+
bracketsNestingLevel++;
136+
if (char === PARAMETER_CLOSE)
137+
bracketsNestingLevel--;
138+
if (char === PARAMETER_CLOSE && bracketsNestingLevel === 0) {
139+
paramStops.push(i);
140+
insideParameter = false;
141+
}
142+
if (cljParser.R_CLJ_WHITE_SPACE.test(char) && bracketsNestingLevel === 0) {
143+
paramStops.push(i);
144+
insideParameter = false;
145+
}
146+
}
147+
}
148+
paramStops.push(signatureParameter.length);
149+
150+
return paramStarts
151+
.map((paramStart, index) => signatureParameter.substring(paramStart, paramStops[index] + 1))
152+
.map(parameter => new vscode.ParameterInformation(parameter));
153+
}

0 commit comments

Comments
 (0)