-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Fetch tooltip details on-demand for auto-completions #368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 28 commits
7675901
eb42669
2756974
c2c1ced
a108c96
14864a5
0ed51d6
51b544c
3cd11e6
82e0ad1
9295c1a
06eb1a5
e9db8e0
d12ca03
d8ab041
db75cd0
9ab2c47
d9c95d0
d587485
1da5e0a
3ac66b6
94a5a7e
df5af0e
af5c648
fdb0e57
c67ac49
3f27b3b
7163a1e
2c8b179
3689667
e193805
ed48871
92655ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,76 +1,35 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove the license header, they should be added for new files. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only new files? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, apparently that's what CELA have informed Brett. |
||
'use strict'; | ||
|
||
import * as vscode from 'vscode'; | ||
import { Position, ProviderResult, SnippetString, Uri } from 'vscode'; | ||
import { PythonSettings } from '../common/configSettings'; | ||
import { Tokenizer } from '../language/tokenizer'; | ||
import { TokenType } from '../language/types'; | ||
import { isTestExecution } from '../common/configSettings'; | ||
import { JediFactory } from '../languageServices/jediProxyFactory'; | ||
import { captureTelemetry } from '../telemetry'; | ||
import { COMPLETION } from '../telemetry/constants'; | ||
import { extractSignatureAndDocumentation } from './jediHelpers'; | ||
import * as proxy from './jediProxy'; | ||
import { CompletionSource } from './completionSource'; | ||
|
||
export class PythonCompletionItemProvider implements vscode.CompletionItemProvider { | ||
private completionSource: CompletionSource; | ||
|
||
public constructor(private jediFactory: JediFactory) { } | ||
private static parseData(data: proxy.ICompletionResult, resource: Uri): vscode.CompletionItem[] { | ||
if (data && data.items.length > 0) { | ||
return data.items.map(item => { | ||
const sigAndDocs = extractSignatureAndDocumentation(item); | ||
const completionItem = new vscode.CompletionItem(item.text); | ||
completionItem.kind = item.type; | ||
completionItem.documentation = sigAndDocs[1].length === 0 ? item.description : sigAndDocs[1]; | ||
completionItem.detail = sigAndDocs[0].split(/\r?\n/).join(''); | ||
if (PythonSettings.getInstance(resource).autoComplete.addBrackets === true && | ||
(item.kind === vscode.SymbolKind.Function || item.kind === vscode.SymbolKind.Method)) { | ||
completionItem.insertText = new SnippetString(item.text).appendText('(').appendTabstop().appendText(')'); | ||
} | ||
|
||
// ensure the built in memebers are at the bottom | ||
completionItem.sortText = (completionItem.label.startsWith('__') ? 'z' : (completionItem.label.startsWith('_') ? 'y' : '__')) + completionItem.label; | ||
return completionItem; | ||
}); | ||
} | ||
return []; | ||
constructor(jediFactory: JediFactory) { | ||
this.completionSource = new CompletionSource(jediFactory); | ||
} | ||
|
||
@captureTelemetry(COMPLETION) | ||
public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): ProviderResult<vscode.CompletionItem[]> { | ||
if (position.character <= 0) { | ||
return Promise.resolve([]); | ||
} | ||
const filename = document.fileName; | ||
const lineText = document.lineAt(position.line).text; | ||
if (lineText.match(/^\s*\/\//)) { | ||
return Promise.resolve([]); | ||
public async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): | ||
Promise<vscode.CompletionItem[]> { | ||
const items = await this.completionSource.getVsCodeCompletionItems(document, position, token); | ||
if (isTestExecution()) { | ||
for (let i = 0; i < Math.min(3, items.length); i += 1) { | ||
items[i] = await this.resolveCompletionItem(items[i], token); | ||
} | ||
} | ||
// Suppress completion inside string and comments | ||
if (this.isPositionInsideStringOrComment(document, position)) { | ||
return Promise.resolve([]); | ||
} | ||
const type = proxy.CommandType.Completions; | ||
const columnIndex = position.character; | ||
|
||
const source = document.getText(); | ||
const cmd: proxy.ICommand<proxy.ICommandResult> = { | ||
command: type, | ||
fileName: filename, | ||
columnIndex: columnIndex, | ||
lineIndex: position.line, | ||
source: source | ||
}; | ||
|
||
return this.jediFactory.getJediProxyHandler<proxy.ICompletionResult>(document.uri).sendCommand(cmd, token).then(data => { | ||
return PythonCompletionItemProvider.parseData(data, document.uri); | ||
}); | ||
return items; | ||
} | ||
|
||
private isPositionInsideStringOrComment(document: vscode.TextDocument, position: vscode.Position): boolean { | ||
const tokenizeTo = position.translate(1, 0); | ||
const text = document.getText(new vscode.Range(new Position(0, 0), tokenizeTo)); | ||
const t = new Tokenizer(); | ||
const tokens = t.Tokenize(text); | ||
const index = tokens.getItemContaining(document.offsetAt(position)); | ||
return index >= 0 && (tokens[index].TokenType === TokenType.String || tokens[index].TokenType === TokenType.Comment); | ||
public async resolveCompletionItem(item: vscode.CompletionItem, token: vscode.CancellationToken): Promise<vscode.CompletionItem> { | ||
item.documentation = await this.completionSource.getDocumentation(item, token); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably check if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Previous code used to initialize both the I just tested the impact of the change, we loose the signature.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe VSC is not calling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yes, that's true. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the last item (colored version). |
||
return item; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
'use strict'; | ||
|
||
import * as vscode from 'vscode'; | ||
import { PythonSettings } from '../common/configSettings'; | ||
import { Tokenizer } from '../language/tokenizer'; | ||
import { TokenType } from '../language/types'; | ||
import { JediFactory } from '../languageServices/jediProxyFactory'; | ||
import { HoverSource } from './hoverSource'; | ||
import * as proxy from './jediProxy'; | ||
|
||
class DocumentPosition { | ||
constructor(public document: vscode.TextDocument, public position: vscode.Position) { } | ||
|
||
public static fromObject(item: object): DocumentPosition { | ||
// tslint:disable-next-line:no-any | ||
return (item as any).documentPosition as DocumentPosition; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we change the name of the property to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK |
||
} | ||
|
||
public attachTo(item: object): void { | ||
// tslint:disable-next-line:no-any | ||
(item as any).documentPosition = this; | ||
} | ||
} | ||
|
||
export class CompletionSource { | ||
private jediFactory: JediFactory; | ||
private hoverSource: HoverSource; | ||
|
||
constructor(jediFactory: JediFactory) { | ||
this.jediFactory = jediFactory; | ||
this.hoverSource = new HoverSource(jediFactory); | ||
} | ||
|
||
public async getVsCodeCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) | ||
: Promise<vscode.CompletionItem[]> { | ||
const result = await this.getCompletionResult(document, position, token); | ||
if (result === undefined) { | ||
return Promise.resolve([]); | ||
} | ||
return this.toVsCodeCompletions(new DocumentPosition(document, position), result, document.uri); | ||
} | ||
|
||
public async getDocumentation(completionItem: vscode.CompletionItem, token: vscode.CancellationToken): Promise<string> { | ||
const documentPosition = DocumentPosition.fromObject(completionItem); | ||
if (documentPosition === undefined) { | ||
Promise.resolve<string>(''); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean to return something? This is a no-op statement. return ''; |
||
} | ||
|
||
// Supply hover source with simulated document text where item in question was 'already typed'. | ||
const document = documentPosition.document; | ||
const position = documentPosition.position; | ||
const itemText = completionItem.insertText ? completionItem.insertText : completionItem.label; | ||
const wordRange = document.getWordRangeAtPosition(position); | ||
|
||
const leadingRange = wordRange !== undefined | ||
? new vscode.Range(new vscode.Position(0, 0), wordRange.start) | ||
: new vscode.Range(new vscode.Position(0, 0), position); | ||
|
||
const itemString = `${itemText}`; | ||
const sourceText = `${document.getText(leadingRange)}${itemString}`; | ||
const range = new vscode.Range(leadingRange.end, leadingRange.end.translate(0, itemString.length)); | ||
|
||
const hoverStrings = await this.hoverSource.getHoverStringsFromText(document.uri, document.fileName, range, sourceText, token); | ||
if (!hoverStrings || hoverStrings.length === 0) { | ||
return ''; | ||
} | ||
return hoverStrings.join('\n'); | ||
} | ||
|
||
private async getCompletionResult(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) | ||
: Promise<proxy.ICompletionResult | undefined> { | ||
if (position.character <= 0) { | ||
return undefined; | ||
} | ||
const filename = document.fileName; | ||
const lineText = document.lineAt(position.line).text; | ||
if (lineText.match(/^\s*\/\//)) { | ||
return undefined; | ||
} | ||
// Suppress completion inside string and comments | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please end sentences with periods ( |
||
if (this.isPositionInsideStringOrComment(document, position)) { | ||
return undefined; | ||
} | ||
const type = proxy.CommandType.Completions; | ||
const columnIndex = position.character; | ||
|
||
const source = document.getText(); | ||
const cmd: proxy.ICommand<proxy.ICommandResult> = { | ||
command: type, | ||
fileName: filename, | ||
columnIndex: columnIndex, | ||
lineIndex: position.line, | ||
source: source | ||
}; | ||
|
||
return await this.jediFactory.getJediProxyHandler<proxy.ICompletionResult>(document.uri).sendCommand(cmd, token); | ||
} | ||
|
||
private toVsCodeCompletions(documentPosition: DocumentPosition, data: proxy.ICompletionResult, resource: vscode.Uri): vscode.CompletionItem[] { | ||
return data && data.items.length > 0 ? data.items.map(item => this.toVsCodeCompletion(documentPosition, item, resource)) : []; | ||
} | ||
|
||
private toVsCodeCompletion(documentPosition: DocumentPosition, item: proxy.IAutoCompleteItem, resource: vscode.Uri): vscode.CompletionItem { | ||
const completionItem = new vscode.CompletionItem(item.text); | ||
completionItem.kind = item.type; | ||
if (PythonSettings.getInstance(resource).autoComplete.addBrackets === true && | ||
(item.kind === vscode.SymbolKind.Function || item.kind === vscode.SymbolKind.Method)) { | ||
completionItem.insertText = new vscode.SnippetString(item.text).appendText('(').appendTabstop().appendText(')'); | ||
} | ||
// ensure the built in members are at the bottom | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please end sentence with period ( |
||
completionItem.sortText = (completionItem.label.startsWith('__') ? 'z' : (completionItem.label.startsWith('_') ? 'y' : '__')) + completionItem.label; | ||
documentPosition.attachTo(completionItem); | ||
return completionItem; | ||
} | ||
|
||
private isPositionInsideStringOrComment(document: vscode.TextDocument, position: vscode.Position): boolean { | ||
const tokenizeTo = position.translate(1, 0); | ||
const text = document.getText(new vscode.Range(new vscode.Position(0, 0), tokenizeTo)); | ||
const t = new Tokenizer(); | ||
const tokens = t.Tokenize(text); | ||
const index = tokens.getItemContaining(document.offsetAt(position)); | ||
return index >= 0 && (tokens[index].TokenType === TokenType.String || tokens[index].TokenType === TokenType.Comment); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason to keep this (and the other commented-out code) around?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just in case we need it (or for reference purposes - i.e. how to do the doc fetch). But I am fine removing it, up to you folks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I say strip it; we have a VCS for a reason. 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK