Skip to content

Commit 1212945

Browse files
committed
added signature provider (tooltips for arguments of functions/methods) #71
1 parent a2cd5e1 commit 1212945

7 files changed

Lines changed: 189 additions & 31 deletions

File tree

pythonFiles/completion.py

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,45 @@ def _get_call_signatures(self, script):
9595
_signatures.append((signature, name, value))
9696
return _signatures
9797

98+
def _get_call_signatures_with_args(self, script):
99+
"""Extract call signatures from jedi.api.Script object in failsafe way.
100+
101+
Returns:
102+
Array with dictionary
103+
"""
104+
_signatures = []
105+
try:
106+
call_signatures = script.call_signatures()
107+
except KeyError:
108+
call_signatures = []
109+
for signature in call_signatures:
110+
sig = {"name": "", "description": "", "docstring": "",
111+
"paramindex": 0, "params": [], "bracketstart": []}
112+
sig["description"] = signature.description
113+
sig["docstring"] = signature.docstring()
114+
sig["name"] = signature.name
115+
sig["paramindex"] = signature.index
116+
sig["bracketstart"].append(signature.index)
117+
118+
_signatures.append(sig)
119+
for pos, param in enumerate(signature.params):
120+
if not param.name:
121+
continue
122+
if param.name == 'self' and pos == 0:
123+
continue
124+
if WORD_RE.match(param.name) is None:
125+
continue
126+
try:
127+
name, value = param.description.split('=')
128+
except ValueError:
129+
name = param.description
130+
value = None
131+
#if name.startswith('*'):
132+
# continue
133+
#_signatures.append((signature, name, value))
134+
sig["params"].append({"name": name, "value":value, "docstring":param.docstring(), "description":param.description})
135+
return _signatures
136+
98137
def _serialize_completions(self, script, identifier=None, prefix=''):
99138
"""Serialize response to be read from VSCode.
100139
@@ -164,23 +203,7 @@ def _serialize_arguments(self, script, identifier=None):
164203
Returns:
165204
Serialized string to send to VSCode.
166205
"""
167-
seen = set()
168-
arguments = []
169-
i = 1
170-
for _, name, value in self._get_call_signatures(script):
171-
if not value:
172-
arg = '${%s:%s}' % (i, name)
173-
elif self.use_snippets == 'all':
174-
arg = '%s=${%s:%s}' % (name, i, value)
175-
else:
176-
continue
177-
if name not in seen:
178-
seen.add(name)
179-
arguments.append(arg)
180-
i += 1
181-
snippet = '%s$0' % ', '.join(arguments)
182-
return json.dumps({'id': identifier, 'results': [],
183-
'arguments': snippet})
206+
return json.dumps({"id": identifier, "results" : self._get_call_signatures_with_args(script) })
184207

185208
def _serialize_definitions(self, definitions, identifier=None):
186209
"""Serialize response to be read from VSCode.

src/client/extension.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {PythonFormattingEditProvider} from "./providers/formatProvider";
1010
import * as sortImports from "./sortImports";
1111
import {LintProvider} from "./providers/lintProvider";
1212
import {PythonSymbolProvider} from "./providers/symbolProvider";
13+
import {PythonSignatureProvider} from "./providers/signatureProvider";
1314
import {activateFormatOnSaveProvider} from "./providers/formatOnSaveProvider";
1415
import * as path from "path";
1516
import * as settings from "./common/configSettings";
@@ -56,13 +57,14 @@ export function activate(context: vscode.ExtensionContext) {
5657
]
5758
});
5859

59-
context.subscriptions.push(vscode.languages.registerRenameProvider(PYTHON, new PythonRenameProvider(context)));
60+
let renameProvider = new PythonRenameProvider(context);
61+
context.subscriptions.push(vscode.languages.registerRenameProvider(PYTHON, renameProvider));
6062
context.subscriptions.push(vscode.languages.registerHoverProvider(PYTHON, new PythonHoverProvider(context)));
6163
context.subscriptions.push(vscode.languages.registerDefinitionProvider(PYTHON, new PythonDefinitionProvider(context)));
6264
context.subscriptions.push(vscode.languages.registerReferenceProvider(PYTHON, new PythonReferenceProvider(context)));
6365
context.subscriptions.push(vscode.languages.registerCompletionItemProvider(PYTHON, new PythonCompletionItemProvider(context), "."));
64-
context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(PYTHON, new PythonSymbolProvider(context)));
65-
66+
context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(PYTHON, new PythonSymbolProvider(context, renameProvider.JediProxy)));
67+
context.subscriptions.push(vscode.languages.registerSignatureHelpProvider(PYTHON, new PythonSignatureProvider(context, renameProvider.JediProxy), "(", ","));
6668
context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider(PYTHON, new PythonFormattingEditProvider(context, formatOutChannel, pythonSettings, vscode.workspace.rootPath)));
6769
context.subscriptions.push(new LintProvider(context, lintingOutChannel, vscode.workspace.rootPath));
6870
}

src/client/providers/jediProxy.ts

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,16 @@ function spawnProcess(dir: string) {
309309
}
310310
break;
311311
}
312+
case CommandType.Arguments: {
313+
let defs = <any[]>response["results"];
314+
if (defs.length > 0) {
315+
cmd.resolve(<IArgumentsResult>{
316+
requestId: cmd.id,
317+
definitions: defs
318+
});
319+
}
320+
break;
321+
}
312322
}
313323
}
314324

@@ -472,7 +482,24 @@ export interface IReferenceResult extends ICommandResult {
472482
references: IReference[];
473483
}
474484
export interface ISymbolResult extends ICommandResult {
475-
definitions: IDefinition[]
485+
definitions: IDefinition[];
486+
}
487+
export interface IArgumentsResult extends ICommandResult {
488+
definitions: ISignature[];
489+
}
490+
491+
export interface ISignature {
492+
name: string;
493+
docstring: string;
494+
description: string;
495+
paramindex: number;
496+
params: IArgument[];
497+
}
498+
export interface IArgument {
499+
name: string;
500+
value: string;
501+
docstring: string;
502+
description: string;
476503
}
477504

478505
export interface IReference {
@@ -511,15 +538,24 @@ export class JediProxyHandler<R extends ICommandResult, T> {
511538
private parseResponse: (data: R) => T;
512539
private cancellationTokenSource: vscode.CancellationTokenSource;
513540

514-
public constructor(context: vscode.ExtensionContext, defaultCallbackData: T, parseResponse: (data: R) => T) {
515-
if (pythonSettings.devOptions.indexOf("SINGLE_JEDI") >= 0) {
516-
if (jediProxy_singleton === null) {
517-
jediProxy_singleton = new JediProxy(context);
518-
}
519-
this.jediProxy = jediProxy_singleton;
541+
public get JediProxy(): JediProxy {
542+
return this.jediProxy;
543+
}
544+
545+
public constructor(context: vscode.ExtensionContext, defaultCallbackData: T, parseResponse: (data: R) => T, jediProxy: JediProxy = null) {
546+
if (jediProxy) {
547+
this.jediProxy = jediProxy;
520548
}
521549
else {
522-
this.jediProxy = new JediProxy(context);
550+
if (pythonSettings.devOptions.indexOf("SINGLE_JEDI") >= 0) {
551+
if (jediProxy_singleton === null) {
552+
jediProxy_singleton = new JediProxy(context);
553+
}
554+
this.jediProxy = jediProxy_singleton;
555+
}
556+
else {
557+
this.jediProxy = new JediProxy(context);
558+
}
523559
}
524560
this.defaultCallbackData = defaultCallbackData;
525561
this.parseResponse = parseResponse;

src/client/providers/renameProvider.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ var _newName = "";
99

1010
export class PythonRenameProvider implements vscode.RenameProvider {
1111
private jediProxyHandler: proxy.JediProxyHandler<proxy.IReferenceResult, vscode.WorkspaceEdit>;
12-
12+
public get JediProxy(): proxy.JediProxy {
13+
return this.jediProxyHandler.JediProxy;
14+
}
1315
public constructor(context: vscode.ExtensionContext) {
1416
this.jediProxyHandler = new proxy.JediProxyHandler(context, null, PythonRenameProvider.parseData);
1517
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"use strict";
2+
3+
import * as vscode from "vscode";
4+
import {TextDocument, Position, CancellationToken, SignatureHelp, ExtensionContext} from "vscode";
5+
import * as proxy from "./jediProxy";
6+
import * as telemetryContracts from "../common/telemetryContracts";
7+
8+
const DOCSTRING_PARAM_PATTERNS = [
9+
"\\s*:type\\s*PARAMNAME:\\s*([^\\n, ]+)", // Sphinx
10+
"\\s*:param\\s*(\\w?)\\s*PARAMNAME:[^\\n]+", // Sphinx param with type
11+
"\\s*@type\\s*PARAMNAME:\\s*([^\\n, ]+)" // Epydoc
12+
];
13+
14+
/**
15+
* Extrct the documentation for parameters from a given docstring
16+
*
17+
* @param {string} paramName Name of the parameter
18+
* @param {string} docString The docstring for the function
19+
* @returns {string} Docstring for the parameter
20+
*/
21+
function extractParamDocString(paramName: string, docString: string): string {
22+
let paramDocString = "";
23+
// In docstring the '*' is escaped with a backslash
24+
paramName = paramName.replace(new RegExp("\\*", "g"), "\\\\\\*");
25+
26+
DOCSTRING_PARAM_PATTERNS.forEach(pattern => {
27+
if (paramDocString.length > 0) {
28+
return;
29+
}
30+
pattern = pattern.replace("PARAMNAME", paramName);
31+
let regExp = new RegExp(pattern);
32+
let matches = regExp.exec(docString);
33+
if (matches && matches.length > 0) {
34+
paramDocString = matches[0];
35+
if (paramDocString.indexOf(":") >= 0) {
36+
paramDocString = paramDocString.substring(paramDocString.indexOf(":") + 1);
37+
}
38+
if (paramDocString.indexOf(":") >= 0) {
39+
paramDocString = paramDocString.substring(paramDocString.indexOf(":") + 1);
40+
}
41+
}
42+
});
43+
44+
return paramDocString.trim();
45+
}
46+
export class PythonSignatureProvider implements vscode.SignatureHelpProvider {
47+
private jediProxyHandler: proxy.JediProxyHandler<proxy.IArgumentsResult, vscode.SignatureHelp>;
48+
49+
public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) {
50+
this.jediProxyHandler = new proxy.JediProxyHandler(context, null, PythonSignatureProvider.parseData, jediProxy);
51+
}
52+
private static parseData(data: proxy.IArgumentsResult): vscode.SignatureHelp {
53+
if (data && Array.isArray(data.definitions) && data.definitions.length > 0) {
54+
let signature = new SignatureHelp();
55+
signature.activeSignature = 0;
56+
57+
data.definitions.forEach(def => {
58+
signature.activeParameter = def.paramindex;
59+
// Don't display the documentation, as vs code doesn't format the docmentation
60+
// i.e. line feeds are not respected, long content is stripped
61+
let sig = <vscode.SignatureInformation>{
62+
// documentation: def.docstring,
63+
label: def.description,
64+
parameters: []
65+
};
66+
sig.parameters = def.params.map(arg => {
67+
if (arg.docstring.length === 0) {
68+
arg.docstring = extractParamDocString(arg.name, def.docstring);
69+
}
70+
return <vscode.ParameterInformation>{
71+
documentation: arg.docstring.length > 0 ? arg.docstring : arg.description,
72+
label: arg.description.length > 0 ? arg.description : arg.name
73+
};
74+
});
75+
signature.signatures.push(sig);
76+
});
77+
return signature;
78+
}
79+
80+
return new SignatureHelp();
81+
}
82+
provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken): Thenable<SignatureHelp> {
83+
return new Promise<SignatureHelp>((resolve, reject) => {
84+
let cmd: proxy.ICommand<proxy.IArgumentsResult> = {
85+
telemetryEvent: telemetryContracts.IDE.Symbol,
86+
command: proxy.CommandType.Arguments,
87+
fileName: document.fileName,
88+
columnIndex: position.character,
89+
lineIndex: position.line,
90+
source: document.getText()
91+
};
92+
this.jediProxyHandler.sendCommand(cmd, resolve, token);
93+
});
94+
}
95+
}

src/client/providers/symbolProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import * as telemetryContracts from "../common/telemetryContracts";
77
export class PythonSymbolProvider implements vscode.DocumentSymbolProvider {
88
private jediProxyHandler: proxy.JediProxyHandler<proxy.ISymbolResult, vscode.SymbolInformation[]>;
99

10-
public constructor(context: vscode.ExtensionContext) {
11-
this.jediProxyHandler = new proxy.JediProxyHandler(context, null, PythonSymbolProvider.parseData);
10+
public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) {
11+
this.jediProxyHandler = new proxy.JediProxyHandler(context, null, PythonSymbolProvider.parseData, jediProxy);
1212
}
1313
private static parseData(data: proxy.ISymbolResult): vscode.SymbolInformation[] {
1414
if (data) {

src/test/pythonFiles/docstrings/one.py

Whitespace-only changes.

0 commit comments

Comments
 (0)