diff --git a/pythonFiles/completion.py b/pythonFiles/completion.py index 168584fece10..cd254c201304 100644 --- a/pythonFiles/completion.py +++ b/pythonFiles/completion.py @@ -266,14 +266,14 @@ def _extract_range(self, definition): from jedi import common from jedi.parser.utils import load_parser # get the scope range - if definition.type in ['class', 'function', 'method']: - scope = definition._name.get_parent_scope() + if definition.type in ['class', 'function']: + scope = definition._definition start_line = scope.start_pos[0] - 1 start_column = scope.start_pos[1] end_line = scope.end_pos[0] - 1 end_column = scope.end_pos[1] # get the lines - path = definition._name.get_parent_until().path + path = definition._definition.get_parent_until().path parser = load_parser(path) lines = common.splitlines(parser.source) lines[end_line] = lines[end_line][:end_column] @@ -334,27 +334,26 @@ def _serialize_definitions(self, definitions, identifier=None): def _serialize_tooltip(self, definitions, identifier=None): _definitions = [] for definition in definitions: - if definition.module_path: - if definition.type == 'import': - definition = self._top_definition(definition) - if not definition.module_path: - continue - - description = definition.docstring() - if description is not None: - description = description.strip() - if not description: - description = self._additional_info(definition) - _definition = { - 'text': definition.name, - 'type': self._get_definition_type(definition), - 'fileName': definition.module_path, - 'description': description, - 'line': definition.line - 1, - 'column': definition.column - } - _definitions.append(_definition) - break + signature = definition.name + description = None + if definition.type in ['class', 'function']: + signature = self._generate_signature(definition) + description = definition.docstring(raw=True).strip() + if not description and not definition.get_line_code(): + # jedi returns an empty string for compiled objects + description = definition.docstring().strip() + if definition.type == 'module': + signature = definition.full_name + description = definition.docstring(raw=True).strip() + if not description and not definition.get_line_code(): + # jedi returns an empty string for compiled objects + description = definition.docstring().strip() + _definition = { + 'type': self._get_definition_type(definition), + 'description': description, + 'signature': signature + } + _definitions.append(_definition) return json.dumps({'id': identifier, 'results': _definitions}) def _serialize_usages(self, usages, identifier=None): @@ -429,7 +428,7 @@ def _process_request(self, request): script.goto_assignments(), request['id'])) if lookup == 'tooltip': return self._write_response(self._serialize_tooltip( - script.goto_assignments(), request['id'])) + script.goto_definitions(), request['id'])) elif lookup == 'arguments': return self._write_response(self._serialize_arguments( script, request['id'])) diff --git a/src/client/providers/completionProvider.ts b/src/client/providers/completionProvider.ts index 5ae5e000301d..9d136ce52a69 100644 --- a/src/client/providers/completionProvider.ts +++ b/src/client/providers/completionProvider.ts @@ -10,7 +10,7 @@ import { PythonSettings } from '../common/configSettings'; const pythonSettings = PythonSettings.getInstance(); export class PythonCompletionItemProvider implements vscode.CompletionItemProvider { - private jediProxyHandler: proxy.JediProxyHandler; + private jediProxyHandler: proxy.JediProxyHandler; public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); diff --git a/src/client/providers/definitionProvider.ts b/src/client/providers/definitionProvider.ts index c257a093e6ae..616215e19876 100644 --- a/src/client/providers/definitionProvider.ts +++ b/src/client/providers/definitionProvider.ts @@ -5,7 +5,7 @@ import * as proxy from './jediProxy'; import * as telemetryContracts from "../common/telemetryContracts"; export class PythonDefinitionProvider implements vscode.DefinitionProvider { - private jediProxyHandler: proxy.JediProxyHandler; + private jediProxyHandler: proxy.JediProxyHandler; public get JediProxy(): proxy.JediProxy { return this.jediProxyHandler.JediProxy; } diff --git a/src/client/providers/hoverProvider.ts b/src/client/providers/hoverProvider.ts index f52c3e8f2991..acee1f80bfcc 100644 --- a/src/client/providers/hoverProvider.ts +++ b/src/client/providers/hoverProvider.ts @@ -3,58 +3,65 @@ import * as vscode from 'vscode'; import * as proxy from './jediProxy'; import * as telemetryContracts from "../common/telemetryContracts"; -import { extractHoverInfo} from './jediHelpers'; export class PythonHoverProvider implements vscode.HoverProvider { - private jediProxyHandler: proxy.JediProxyHandler; + private jediProxyHandler: proxy.JediProxyHandler; public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); } - public provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable { + private static parseData(data: proxy.IHoverResult): vscode.Hover { + let results = []; + data.items.forEach(item => { + let { description, signature } = item; + switch (item.kind) { + case vscode.SymbolKind.Constructor: + case vscode.SymbolKind.Function: + case vscode.SymbolKind.Method: { + signature = 'def ' + signature; + break; + } + case vscode.SymbolKind.Class: { + signature = 'class ' + signature; + break; + } + } + results.push({language: 'python', value: signature}); + if (item.description) { + results.push(description); + } + }); + return new vscode.Hover(results); + } + public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { var filename = document.fileName; if (document.lineAt(position.line).text.match(/^\s*\/\//)) { - return Promise.resolve(null); + return null; } if (position.character <= 0) { - return Promise.resolve(null); + return null; } var range = document.getWordRangeAtPosition(position); if (!range || range.isEmpty) { - return Promise.resolve(null); + return null; } - var columnIndex = range.start.character < range.end.character ? range.start.character + 2 : range.end.character; - var cmd: proxy.ICommand = { - command: proxy.CommandType.Completions, + + var cmd: proxy.ICommand = { + command: proxy.CommandType.Hover, fileName: filename, - columnIndex: columnIndex, + columnIndex: range.end.character, lineIndex: position.line }; if (document.isDirty) { cmd.source = document.getText(); } - return this.jediProxyHandler.sendCommand(cmd, token).then(data => { - if (!data || !Array.isArray(data.items) || data.items.length === 0) { - return; - } - // Find the right items - const wordUnderCursor = document.getText(range); - const completionItem = data.items.filter(item => item.text === wordUnderCursor); - if (completionItem.length === 0) { - return; - } - var definition = completionItem[0]; - var txt = definition.description || definition.text; - if (typeof txt !== 'string' || txt.length === 0) { - return; - } - if (wordUnderCursor === txt) { - return; - } + const data = await this.jediProxyHandler.sendCommand(cmd, token); + if (!data || !data.items.length) { + return; + } - return extractHoverInfo(definition); - }); + return PythonHoverProvider.parseData(data); } } diff --git a/src/client/providers/jediHelpers.ts b/src/client/providers/jediHelpers.ts index 42d59d42b137..858b5db859b8 100644 --- a/src/client/providers/jediHelpers.ts +++ b/src/client/providers/jediHelpers.ts @@ -47,12 +47,3 @@ export function extractSignatureAndDocumentation(definition: proxy.IAutoComplete } return [signature, lines.join(EOL).trim().replace(/^\s+|\s+$/g, '').trim()]; } - -export function extractHoverInfo(definition: proxy.IAutoCompleteItem): vscode.Hover { - const parts = extractSignatureAndDocumentation(definition, true); - const hoverInfo: vscode.MarkedString[] = parts[0].length === 0 ? [] : [{ language: 'python', value: parts[0] }]; - if (parts[1].length > 0) { - hoverInfo.push(parts[1]); - } - return new vscode.Hover(hoverInfo); -} \ No newline at end of file diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts index cd11d7b88b40..53a2a339b6d1 100644 --- a/src/client/providers/jediProxy.ts +++ b/src/client/providers/jediProxy.ts @@ -106,6 +106,7 @@ function getMappedVSCodeSymbol(pythonType: string): vscode.SymbolKind { export enum CommandType { Arguments, Completions, + Hover, Usages, Definitions, Symbols @@ -115,6 +116,7 @@ var commandNames = new Map(); commandNames.set(CommandType.Arguments, "arguments"); commandNames.set(CommandType.Completions, "completions"); commandNames.set(CommandType.Definitions, "definitions"); +commandNames.set(CommandType.Hover, "tooltip"); commandNames.set(CommandType.Usages, "usages"); commandNames.set(CommandType.Symbols, "names"); @@ -291,7 +293,7 @@ function spawnProcess(dir: string) { const originalType = item.type; item.type = getMappedVSCodeType(originalType); item.kind = getMappedVSCodeSymbol(originalType); - item.raw_type = getMappedVSCodeType(originalType); + item.rawType = getMappedVSCodeType(originalType); }); let completionResult: ICompletionResult = { @@ -313,6 +315,7 @@ function spawnProcess(dir: string) { defResult.definition = { fileName: def.fileName, text: def.text, + rawType: originalType, type: getMappedVSCodeType(originalType), kind: getMappedVSCodeSymbol(originalType), container: def.container, @@ -328,6 +331,22 @@ function spawnProcess(dir: string) { cmd.deferred.resolve(defResult); break; } + case CommandType.Hover: { + var defs = response['results']; + var defResult: IHoverResult = { + requestId: cmd.id, + items: defs.map(def => { + return { + kind: getMappedVSCodeSymbol(def.type), + description: def.description, + signature: def.signature + } + }) + }; + + cmd.deferred.resolve(defResult); + break; + } case CommandType.Symbols: { var defs = response['results']; defs = Array.isArray(defs) ? defs : []; @@ -340,6 +359,7 @@ function spawnProcess(dir: string) { return { fileName: def.fileName, text: def.text, + rawType: originalType, type: getMappedVSCodeType(originalType), kind: getMappedVSCodeSymbol(originalType), container: def.container, @@ -563,6 +583,9 @@ export interface ICommandResult { export interface ICompletionResult extends ICommandResult { items: IAutoCompleteItem[]; } +export interface IHoverResult extends ICommandResult { + items: IHoverItem[]; +} export interface IDefinitionResult extends ICommandResult { definition: IDefinition; } @@ -600,7 +623,7 @@ export interface IReference { export interface IAutoCompleteItem { type: vscode.CompletionItemKind; - raw_type: vscode.CompletionItemKind; + rawType: vscode.CompletionItemKind; kind: vscode.SymbolKind; text: string; description: string; @@ -614,6 +637,7 @@ interface IDefinitionRange { endColumn: number; } export interface IDefinition { + rawType: string; type: vscode.CompletionItemKind; kind: vscode.SymbolKind; text: string; @@ -622,7 +646,13 @@ export interface IDefinition { range: IDefinitionRange; } -export class JediProxyHandler { +export interface IHoverItem { + kind: vscode.SymbolKind; + description: string; + signature: string; +} + +export class JediProxyHandler { private jediProxy: JediProxy; private lastToken: vscode.CancellationToken; private lastCommandId: number; diff --git a/src/client/providers/referenceProvider.ts b/src/client/providers/referenceProvider.ts index b8b1929152ba..aa9eb7145d83 100644 --- a/src/client/providers/referenceProvider.ts +++ b/src/client/providers/referenceProvider.ts @@ -6,7 +6,7 @@ import * as telemetryContracts from "../common/telemetryContracts"; export class PythonReferenceProvider implements vscode.ReferenceProvider { - private jediProxyHandler: proxy.JediProxyHandler; + private jediProxyHandler: proxy.JediProxyHandler; public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); diff --git a/src/client/providers/signatureProvider.ts b/src/client/providers/signatureProvider.ts index 853541b2a8f4..b3107e3c42bb 100644 --- a/src/client/providers/signatureProvider.ts +++ b/src/client/providers/signatureProvider.ts @@ -44,7 +44,7 @@ function extractParamDocString(paramName: string, docString: string): string { return paramDocString.trim(); } export class PythonSignatureProvider implements vscode.SignatureHelpProvider { - private jediProxyHandler: proxy.JediProxyHandler; + private jediProxyHandler: proxy.JediProxyHandler; public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); diff --git a/src/client/providers/symbolProvider.ts b/src/client/providers/symbolProvider.ts index 0a97019099c3..04d45e343e03 100644 --- a/src/client/providers/symbolProvider.ts +++ b/src/client/providers/symbolProvider.ts @@ -5,7 +5,7 @@ import * as proxy from './jediProxy'; import * as telemetryContracts from "../common/telemetryContracts"; export class PythonSymbolProvider implements vscode.DocumentSymbolProvider { - private jediProxyHandler: proxy.JediProxyHandler; + private jediProxyHandler: proxy.JediProxyHandler; public constructor(context: vscode.ExtensionContext, jediProxy: proxy.JediProxy = null) { this.jediProxyHandler = new proxy.JediProxyHandler(context, jediProxy); diff --git a/src/test/extension.autocomplete.test.ts b/src/test/extension.autocomplete.test.ts index b6218c82ba2e..93b156bb05b1 100644 --- a/src/test/extension.autocomplete.test.ts +++ b/src/test/extension.autocomplete.test.ts @@ -140,7 +140,7 @@ suite('Code Definition', () => { const position = new vscode.Position(30, 5); return vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); + assert.equal(def.length, 1, 'Definition length is incorrect'); assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '17,4', 'Start position is incorrect'); assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '21,11', 'End position is incorrect'); }).then(done, done); @@ -158,7 +158,7 @@ suite('Code Definition', () => { const position = new vscode.Position(1, 5); return vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); + assert.equal(def.length, 1, 'Definition length is incorrect'); assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '0,0', 'Start position is incorrect'); assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '5,11', 'End position is incorrect'); assert.equal(def[0].uri.fsPath, fileTwo, 'File is incorrect'); @@ -177,7 +177,7 @@ suite('Code Definition', () => { const position = new vscode.Position(25, 6); return vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); + assert.equal(def.length, 1, 'Definition length is incorrect'); assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '10,4', 'Start position is incorrect'); assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '16,35', 'End position is incorrect'); assert.equal(def[0].uri.fsPath, fileEncoding, 'File is incorrect'); @@ -196,7 +196,7 @@ suite('Code Definition', () => { const position = new vscode.Position(1, 11); return vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); + assert.equal(def.length, 1, 'Definition length is incorrect'); assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '18,0', 'Start position is incorrect'); assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '23,16', 'End position is incorrect'); assert.equal(def[0].uri.fsPath, fileEncoding, 'File is incorrect'); @@ -231,11 +231,11 @@ suite('Hover Definition', () => { const position = new vscode.Position(30, 5); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); + assert.equal(def.length, 1, 'Definition length is incorrect'); assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '30,4', 'Start position is incorrect'); assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '30,11', 'End position is incorrect'); assert.equal(def[0].contents.length, 2, 'Invalid content items'); - assert.equal(def[0].contents[0]['value'], 'def method1(self)', 'function signature incorrect'); + assert.equal(def[0].contents[0]['value'], 'def method1()', 'function signature incorrect'); assert.equal(def[0].contents[1], `This is method1`, 'Invalid conents'); }).then(done, done); }); @@ -252,7 +252,7 @@ suite('Hover Definition', () => { const position = new vscode.Position(1, 12); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); + assert.equal(def.length, 1, 'Definition length is incorrect'); assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '1,9', 'Start position is incorrect'); assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '1,12', 'End position is incorrect'); assert.equal(def[0].contents[0]['value'], 'def fun()', 'function signature incorrect'); @@ -272,7 +272,7 @@ suite('Hover Definition', () => { const position = new vscode.Position(25, 6); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); + assert.equal(def.length, 1, 'Definition length is incorrect'); assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '25,4', 'Start position is incorrect'); assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '25,7', 'End position is incorrect'); assert.equal(def[0].contents[0]['value'], 'def bar()', 'function signature incorrect'); @@ -293,7 +293,7 @@ suite('Hover Definition', () => { const position = new vscode.Position(1, 11); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); + assert.equal(def.length, 1, 'Definition length is incorrect'); assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '1,5', 'Start position is incorrect'); assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '1,16', 'End position is incorrect'); assert.equal(def[0].contents[0]['value'], 'def showMessage()', 'Invalid content items'); @@ -314,7 +314,7 @@ suite('Hover Definition', () => { const position = new vscode.Position(5, 1); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 0, 'Definition lenght is incorrect'); + assert.equal(def.length, 0, 'Definition length is incorrect'); }).then(done, done); }); @@ -330,7 +330,7 @@ suite('Hover Definition', () => { const position = new vscode.Position(3, 1); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 0, 'Definition lenght is incorrect'); + assert.equal(def.length, 0, 'Definition length is incorrect'); }).then(done, done); }); @@ -346,10 +346,10 @@ suite('Hover Definition', () => { const position = new vscode.Position(11, 15); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); + assert.equal(def.length, 1, 'Definition length is incorrect'); assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '11,12', 'Start position is incorrect'); assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '11,18', 'End position is incorrect'); - assert.equal(def[0].contents[0]['value'], 'class Random(self, x=None)', 'Invalid content items'); + assert.equal(def[0].contents[0]['value'], 'class Random(x=None)', 'Invalid content items'); const documentation = `Random number generator base class used by bound module functions.${EOL}${EOL}` + `Used to instantiate instances of Random to get generators that don't${EOL}` + `share state.${EOL}${EOL}` + @@ -375,10 +375,10 @@ suite('Hover Definition', () => { const position = new vscode.Position(12, 10); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); + assert.equal(def.length, 1, 'Definition length is incorrect'); assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '12,5', 'Start position is incorrect'); assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '12,12', 'End position is incorrect'); - assert.equal(def[0].contents[0]['value'], 'def randint(self, a, b)', 'Invalid content items'); + assert.equal(def[0].contents[0]['value'], 'def randint(a, b)', 'Invalid content items'); const documentation = `Return random integer in range [a, b], including both end points.`; assert.equal(def[0].contents[1], documentation, 'Invalid conents'); }).then(done, done); @@ -396,11 +396,11 @@ suite('Hover Definition', () => { const position = new vscode.Position(8, 14); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); + assert.equal(def.length, 1, 'Definition length is incorrect'); assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '8,11', 'Start position is incorrect'); assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '8,15', 'End position is incorrect'); assert.equal(def[0].contents[0]['value'], 'def acos(x)', 'Invalid content items'); - const documentation = `Return the arc cosine (measured in radians) of x.`; + const documentation = `acos(x)${EOL}${EOL}Return the arc cosine (measured in radians) of x.`; assert.equal(def[0].contents[1], documentation, 'Invalid conents'); }).then(done, done); }); @@ -417,13 +417,32 @@ suite('Hover Definition', () => { const position = new vscode.Position(14, 14); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 1, 'Definition lenght is incorrect'); + assert.equal(def.length, 1, 'Definition length is incorrect'); assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '14,9', 'Start position is incorrect'); assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '14,15', 'End position is incorrect'); - const signature = `class Thread(self, group=None, target=None, name=None,${EOL}args=(), kwargs=None, verbose=None)`; + const signature = `class Thread(group=None, target=None, name=None, args=(), kwargs=None, verbose=None)`; assert.equal(def[0].contents[0]['value'], signature, 'Invalid content items'); const documentation = `A class that represents a thread of control.${EOL}${EOL}This class can be safely subclassed in a limited fashion.`; assert.equal(def[0].contents[1], documentation, 'Invalid conents'); }).then(done, done); }); + + test('Variable', done => { + let textEditor: vscode.TextEditor; + let textDocument: vscode.TextDocument; + return vscode.workspace.openTextDocument(fileHover).then(document => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }).then(editor => { + assert(vscode.window.activeTextEditor, 'No active editor'); + textEditor = editor; + const position = new vscode.Position(6, 2); + return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + }).then(def => { + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(def[0].contents.length, 1, 'Only expected one result'); + const signature = 'Random'; + assert.equal(def[0].contents[0]['value'], signature, 'Invalid content items'); + }).then(done, done); + }); });