Skip to content

Commit

Permalink
feat: inlayHints
Browse files Browse the repository at this point in the history
big thanks to @Zzzen and @yaegassy
  • Loading branch information
fannheyward committed Aug 22, 2022
1 parent 0ca505e commit 1f36838
Show file tree
Hide file tree
Showing 5 changed files with 454 additions and 5 deletions.
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@types/which": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"@zzzen/pyright-internal": "^1.2.0-dev.20220821",
"coc.nvim": "^0.0.82",
"diff-match-patch": "^1.0.5",
"esbuild": "^0.14.2",
Expand Down Expand Up @@ -96,6 +97,21 @@
"default": true,
"description": "Enable coc-pyright extension"
},
"pyright.inlayHints.enable": {
"type": "boolean",
"default": true,
"description": "Enable/disable inlay hints feature"
},
"pyright.inlayHints.functionReturnTypes": {
"type": "boolean",
"default": true,
"description": "Enable/disable inlay hints for function return types"
},
"pyright.inlayHints.variableTypes": {
"type": "boolean",
"default": true,
"description": "Enable/disable inlay hints for variable types"
},
"python.analysis.extraPaths": {
"type": "array",
"default": [],
Expand Down
133 changes: 133 additions & 0 deletions src/features/inlayHints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
CancellationToken,
Emitter,
Event,
Hover,
InlayHint,
InlayHintLabelPart,
InlayHintsProvider,
LanguageClient,
LinesTextDocument,
MarkupContent,
Position,
Range,
workspace,
} from 'coc.nvim';

import * as typeInlayHintsParser from '../parsers/typeInlayHints';

export class TypeInlayHintsProvider implements InlayHintsProvider {
private readonly _onDidChangeInlayHints = new Emitter<void>();
public readonly onDidChangeInlayHints: Event<void> = this._onDidChangeInlayHints.event;

constructor(private client: LanguageClient) {}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async provideInlayHints(document: LinesTextDocument, _range: Range, _token: CancellationToken) {
const inlayHints: InlayHint[] = [];

const code = document.getText();
const parsed = typeInlayHintsParser.parse(code);
if (!parsed) return [];

const walker = new typeInlayHintsParser.TypeInlayHintsWalker();
walker.walk(parsed.parseTree);

for (const item of walker.featureItems) {
if (this.isDisableVariableTypes(item.inlayHintType)) continue;
if (this.isDisableFunctionReturnTypes(item.inlayHintType)) continue;

const startPosition = document.positionAt(item.startOffset);
const endPosition = document.positionAt(item.endOffset);
const hoverResponse = await this.getHoverAtOffset(document, startPosition);

if (hoverResponse) {
let inlayHintLabelValue: string | undefined = undefined;
let inlayHintPosition: Position | undefined = undefined;

if (item.inlayHintType === 'variable') {
inlayHintLabelValue = this.getVariableHintAtHover(hoverResponse);
}

if (item.inlayHintType === 'functionReturn') {
inlayHintLabelValue = this.getFunctionReturnHintAtHover(hoverResponse);
}

if (inlayHintLabelValue) {
const inlayHintLabelPart: InlayHintLabelPart[] = [
{
value: inlayHintLabelValue,
},
];

switch (item.inlayHintType) {
case 'variable':
inlayHintPosition = startPosition;
break;
case 'functionReturn':
inlayHintPosition = endPosition;
break;
default:
break;
}

if (inlayHintPosition) {
const inlayHint: InlayHint = {
label: inlayHintLabelPart,
position: inlayHintPosition,
};

inlayHints.push(inlayHint);
}
}
}
}

return inlayHints;
}

private async getHoverAtOffset(document: LinesTextDocument, position: Position) {
const params = {
textDocument: { uri: document.uri },
position,
};

return await this.client.sendRequest<Hover>('textDocument/hover', params);
}

private getVariableHintAtHover(hover: Hover): string | undefined {
const contents = hover.contents as MarkupContent;
if (contents) {
if (contents.value.includes('(variable)')) {
const text = contents.value.split(': ')[1].split('\n')[0].trim();
const hintText = ': ' + text;
return hintText;
}
}
}

private getFunctionReturnHintAtHover(hover: Hover): string | undefined {
const contents = hover.contents as MarkupContent;
if (contents) {
if (contents.value.includes('(function)') || contents.value.includes('(method)')) {
const text = contents.value.split('->')[1].split('\n')[0].trim();
const hintText = '-> ' + text;
return hintText;
}
}
}

private isDisableVariableTypes(inlayHintType: string) {
if (!workspace.getConfiguration('pyright').get('inlayHints.variableTypes') && inlayHintType === 'variable') {
return true;
}
return false;
}

private isDisableFunctionReturnTypes(inlayHintType: string) {
if (!workspace.getConfiguration('pyright').get('inlayHints.functionReturnTypes') && inlayHintType === 'functionReturn') {
return true;
}
return false;
}
}
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { PythonSettings } from './configSettings';
import { PythonCodeActionProvider } from './features/codeAction';
import { PythonFormattingEditProvider } from './features/formatting';
import { ImportCompletionProvider } from './features/importCompletion';
import { TypeInlayHintsProvider } from './features/inlayHints';
import { sortImports } from './features/isort';
import { LinterProvider } from './features/lintting';
import { addImport, extractMethod, extractVariable } from './features/refactor';
Expand Down Expand Up @@ -138,6 +139,11 @@ export async function activate(context: ExtensionContext): Promise<void> {
const provider = new ImportCompletionProvider();
context.subscriptions.push(languages.registerCompletionItemProvider('python-import', 'PY', ['python'], provider, [' ']));
}
const inlayHints = pyrightCfg.get<boolean>('inlayHints.enable');
if (inlayHints) {
const provider = new TypeInlayHintsProvider(client);
context.subscriptions.push(languages.registerInlayHintsProvider(documentSelector, provider));
}

const textEditorCommands = ['pyright.organizeimports', 'pyright.addoptionalforparam'];
textEditorCommands.forEach((commandName: string) => {
Expand Down
79 changes: 79 additions & 0 deletions src/parsers/typeInlayHints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { printParseNodeType } from '@zzzen/pyright-internal/dist/analyzer/parseTreeUtils';
import { ParseTreeWalker } from '@zzzen/pyright-internal/dist/analyzer/parseTreeWalker';
import { DiagnosticSink } from '@zzzen/pyright-internal/dist/common/diagnosticSink';
import { FunctionNode, MemberAccessNode, NameNode, ParseNode } from '@zzzen/pyright-internal/dist/parser/parseNodes';
import { ParseOptions, Parser, ParseResults } from '@zzzen/pyright-internal/dist/parser/parser';

export function parse(source: string) {
let result: ParseResults | undefined = undefined;
const parserOptions = new ParseOptions();
const diagSink = new DiagnosticSink();
const parser = new Parser();
try {
result = parser.parseSourceFile(source, parserOptions, diagSink);
} catch (e) {}
return result;
}

export type TypeInlayHintsItemType = {
inlayHintType: 'variable' | 'functionReturn';
startOffset: number;
endOffset: number;
value?: string;
};

export class TypeInlayHintsWalker extends ParseTreeWalker {
public featureItems: TypeInlayHintsItemType[] = [];

override visitNode(node: ParseNode) {
return super.visitNode(node);
}

override visitName(node: NameNode): boolean {
if (node.parent) {
const parentNodeType = printParseNodeType(node.parent.nodeType);
// If the type already exists in the code, do not match.
// The parent node is "TypeAnnotation" if the type exists.
if (parentNodeType === 'Assignment') {
this.featureItems.push({
inlayHintType: 'variable',
startOffset: node.start,
endOffset: node.start + node.length - 1,
value: node.value,
});
}
}
return super.visitName(node);
}

override visitMemberAccess(node: MemberAccessNode): boolean {
if (node.parent) {
const parentNodeType = printParseNodeType(node.parent.nodeType);
// If the type already exists in the code, do not match.
// The parent node is "TypeAnnotation" if the type exists.
if (parentNodeType === 'Assignment') {
this.featureItems.push({
inlayHintType: 'variable',
startOffset: node.memberName.start,
endOffset: node.memberName.start + node.memberName.length - 1,
value: node.memberName.value,
});
}
}
return super.visitMemberAccess(node);
}

override visitFunction(node: FunctionNode): boolean {
// If the code describes a type, do not add the item.
// Add item only if "node.returnTypeAnnotation" does not exist.
if (!node.returnTypeAnnotation) {
this.featureItems.push({
inlayHintType: 'functionReturn',
startOffset: node.name.start,
endOffset: node.suite.start,
value: node.name.value,
});
}
return super.visitFunction(node);
}
}
Loading

0 comments on commit 1f36838

Please sign in to comment.