Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
285 changes: 187 additions & 98 deletions packages/pyright-internal/src/languageService/codeActionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,31 @@ import { Commands } from '../commands/commands';
import { throwIfCancellationRequested } from '../common/cancellationUtils';
import { createCommand } from '../common/commandUtils';
import { CreateTypeStubFileAction } from '../common/diagnostic';
import { Range } from '../common/textRange';
import { Range, TextRange } from '../common/textRange';
import { Uri } from '../common/uri/uri';
import { convertToFileTextEdits, convertToTextEditActions, convertToWorkspaceEdit } from '../common/workspaceEditUtils';
import {
convertToFileTextEdits,
convertToTextEditActions,
convertToTextEdits,
convertToWorkspaceEdit,
} from '../common/workspaceEditUtils';
import { Localizer } from '../localization/localize';
import { Workspace } from '../workspaceFactory';
import { CompletionProvider } from './completionProvider';
import { CompletionMap, CompletionProvider } from './completionProvider';
import {
convertOffsetToPosition,
convertPositionToOffset,
convertTextRangeToRange,
getLineEndPosition,
} from '../common/positionUtils';
import { findNodeByOffset } from '../analyzer/parseTreeUtils';
import { ParseNodeType } from '../parser/parseNodes';
import { ParseNode, ParseNodeType } from '../parser/parseNodes';
import { sorter } from '../common/collectionUtils';
import { LanguageServerInterface } from '../common/languageServerInterface';
import { DiagnosticRule } from '../common/diagnosticRules';
import { TypeCategory } from '../analyzer/types';
import { ImportGroup } from '../analyzer/importStatementUtils';
import { TextRangeCollection } from '../common/textRangeCollection';

export class CodeActionProvider {
static mightSupport(kinds: CodeActionKind[] | undefined): boolean {
Expand Down Expand Up @@ -64,117 +73,169 @@ export class CodeActionProvider {
const parseResults = workspace.service.backgroundAnalysisProgram.program.getParseResults(fileUri)!;
const lines = parseResults.tokenizerOutput.lines;

if (diags.find((d) => d.getActions()?.some((action) => action.action === Commands.import))) {
const offset = convertPositionToOffset(range.start, lines);
if (offset === undefined) {
return [];
}
if (!workspace.rootUri) {
return codeActions;
}

const node = findNodeByOffset(parseResults.parserOutput.parseTree, offset);
if (node === undefined) {
return [];
const fs = ls.serviceProvider.fs();
for (const diagnostic of diags) {
const rule = diagnostic.getRule();
if (!rule) {
continue;
}
const line = diagnostic.range.start.line;

const completer = new CompletionProvider(
workspace.service.backgroundAnalysisProgram.program,
fileUri,
convertOffsetToPosition(node.start + node.length, lines),
{
format: 'plaintext',
lazyEdit: false,
snippet: false,
// we don't care about deprecations here so make sure they don't get evaluated unnecessarily
// (we don't call resolveCompletionItem)
checkDeprecatedWhenResolving: true,
useTypingExtensions: workspace.useTypingExtensions,
},
token,
true
);
// ==== CA for auto imports ====
if (diagnostic.getActions()?.some((action) => action.action === Commands.import)) {
const offset = convertPositionToOffset(range.start, lines);
if (offset === undefined) {
return [];
}

const word = node.nodeType === ParseNodeType.Name ? node.d.value : undefined;
const sortedCompletions = completer
.getCompletions()
?.items.filter(
(completion) =>
// only show exact matches as code actions, which matches pylance's behavior. otherwise it's too noisy
// because code actions don't get sorted like completions do. see https://github.com/DetachHead/basedpyright/issues/747
completion.label === word
)
.sort((prev, next) =>
sorter(
prev,
next,
(prev, next) => (prev.sortText && next.sortText && prev.sortText < next.sortText) || false
const node = findNodeByOffset(parseResults.parserOutput.parseTree, offset);
if (node === undefined) {
return [];
}

const completer = this._createCompleter(workspace, fileUri, token, node, lines);

const word = node.nodeType === ParseNodeType.Name ? node.d.value : undefined;
const sortedCompletions = completer
.getCompletions()
?.items.filter(
(completion) =>
// only show exact matches as code actions, which matches pylance's behavior. otherwise it's too noisy
// because code actions don't get sorted like completions do. see https://github.com/DetachHead/basedpyright/issues/747
completion.label === word
)
);
.sort((prev, next) =>
sorter(
prev,
next,
(prev, next) => (prev.sortText && next.sortText && prev.sortText < next.sortText) || false
)
);

for (const suggestedImport of sortedCompletions ?? []) {
if (!suggestedImport.data) {
continue;
}
let textEdits: TextEdit[] = [];
if (suggestedImport.textEdit && 'range' in suggestedImport.textEdit) {
textEdits.push(suggestedImport.textEdit);
for (const suggestedImport of sortedCompletions ?? []) {
if (!suggestedImport.data) {
continue;
}
let textEdits: TextEdit[] = [];
if (suggestedImport.textEdit && 'range' in suggestedImport.textEdit) {
textEdits.push(suggestedImport.textEdit);
}
if (suggestedImport.additionalTextEdits) {
textEdits = textEdits.concat(suggestedImport.additionalTextEdits);
}
if (textEdits.length === 0) {
continue;
}
const workspaceEdit = convertToWorkspaceEdit(
ls.convertUriToLspUriString,
completer.importResolver.fileSystem,
convertToFileTextEdits(fileUri, convertToTextEditActions(textEdits))
);
codeActions.push(
CodeAction.create(
suggestedImport.data.autoImportText.trim(),
workspaceEdit,
CodeActionKind.QuickFix
)
);
}
if (suggestedImport.additionalTextEdits) {
textEdits = textEdits.concat(suggestedImport.additionalTextEdits);
}

// ==== CA for creating type stubs ====
if (diagnostic.getActions()?.some((a) => a.action === Commands.createTypeStub)) {
const action = diagnostic
.getActions()!
.find((a) => a.action === Commands.createTypeStub) as CreateTypeStubFileAction;
if (action) {
const createTypeStubAction = CodeAction.create(
Localizer.CodeAction.createTypeStubFor().format({ moduleName: action.moduleName }),
createCommand(
Localizer.CodeAction.createTypeStub(),
Commands.createTypeStub,
workspace.rootUri.toString(),
action.moduleName,
fileUri.toString()
),
CodeActionKind.QuickFix
);
codeActions.push(createTypeStubAction);
}
if (textEdits.length === 0) {
}

// ==== CA for adding @override decorators ====
if (rule === DiagnosticRule.reportImplicitOverride) {
const lineText = lines.getItemAt(line);
const methodLineContent = parseResults.text.substring(lineText.start, lineText.start + lineText.length);

const indentMatch = methodLineContent.match(/^(\s*)(async\s+)?def\s/);
if (!indentMatch) {
continue;
}
const workspaceEdit = convertToWorkspaceEdit(
ls.convertUriToLspUriString,
completer.importResolver.fileSystem,
convertToFileTextEdits(fileUri, convertToTextEditActions(textEdits))

const indent = indentMatch[1];
const decoratorLine = { line: line - 1, character: 0 };
const offset = convertPositionToOffset(decoratorLine, lines);
if (offset === undefined) {
return [];
}

const node = findNodeByOffset(parseResults.parserOutput.parseTree, offset);
if (node === undefined) {
return [];
}

const decoratorTextEdit: TextEdit = {
range: {
start: { line: decoratorLine.line, character: indent.length },
end: { line: decoratorLine.line, character: indent.length },
},
newText: `\n${indent}@override`,
};

const textEdits: TextEdit[] = [decoratorTextEdit];

const completer = this._createCompleter(workspace, fileUri, token, node, lines);
const completionMap = new CompletionMap();

const overrideDecorator = workspace.service.backgroundAnalysisProgram.program.evaluator!.getTypingType(
node,
'override'
);
if (overrideDecorator?.category === TypeCategory.Function) {
const importTextEditInfo = completer
.createAutoImporter(completionMap, false)
.getTextEditsForAutoImportByFilePath(
{ name: 'override' },
{ name: overrideDecorator.shared.moduleName },
'override',
ImportGroup.BuiltIn,
overrideDecorator.shared.declaration!.uri
);

if (importTextEditInfo.edits) {
textEdits.push(...convertToTextEdits(importTextEditInfo.edits));
}
}

codeActions.push(
CodeAction.create(
suggestedImport.data.autoImportText.trim(),
workspaceEdit,
Localizer.CodeAction.addExplicitOverride(),
convertToWorkspaceEdit(
ls.convertUriToLspUriString,
fs,
convertToFileTextEdits(fileUri, convertToTextEditActions(textEdits))
),
CodeActionKind.QuickFix
)
);
}
}

if (!workspace.rootUri) {
return codeActions;
}

const typeStubDiag = diags.find((d) => {
const actions = d.getActions();
return actions && actions.find((a) => a.action === Commands.createTypeStub);
});

if (typeStubDiag) {
const action = typeStubDiag
.getActions()!
.find((a) => a.action === Commands.createTypeStub) as CreateTypeStubFileAction;
if (action) {
const createTypeStubAction = CodeAction.create(
Localizer.CodeAction.createTypeStubFor().format({ moduleName: action.moduleName }),
createCommand(
Localizer.CodeAction.createTypeStub(),
Commands.createTypeStub,
workspace.rootUri.toString(),
action.moduleName,
fileUri.toString()
),
CodeActionKind.QuickFix
);
codeActions.push(createTypeStubAction);
}
}

const fs = ls.serviceProvider.fs();
for (const diagnostic of diags) {
const rule = diagnostic.getRule();
if (!rule) {
continue;
}
// ==== CA for adding ignore comments ====
const ignoreCommentPrefix = `# pyright: ignore`;
const line = diagnostic.range.start.line;
// we deliberately only check for pyright:ignore comments here but not type:ignore for 2 reasons:
// - type:ignore comments are discouraged in favor of pyright:ignore comments
// - the type:ignore comment might be for another type checker
Expand All @@ -189,12 +250,15 @@ export class CodeActionProvider {
}
positionCharacter = convertTextRangeToRange(lastRuleTextRange, lines).end.character;
insertText = `, ${rule}`;
title = `Add \`${rule}\` to existing \`${ignoreCommentPrefix}\` comment`;
title = Localizer.CodeAction.addIgnoreCommentToExisting().format({
rule: rule,
ignoreCommentPrefix: ignoreCommentPrefix,
});
} else {
positionCharacter = getLineEndPosition(parseResults.tokenizerOutput, parseResults.text, line).character;
const ignoreComment = `${ignoreCommentPrefix}[${rule}]`;
insertText = ` ${ignoreComment}`;
title = `Add \`${ignoreComment}\``;
title = Localizer.CodeAction.addIgnoreComment().format({ ignoreComment: ignoreComment });
}
const position = { line, character: positionCharacter };
codeActions.push(
Expand All @@ -217,4 +281,29 @@ export class CodeActionProvider {

return codeActions;
}

private static _createCompleter(
workspace: Workspace,
fileUri: Uri,
token: CancellationToken,
node: ParseNode,
lines: TextRangeCollection<TextRange>
) {
return new CompletionProvider(
workspace.service.backgroundAnalysisProgram.program,
fileUri,
convertOffsetToPosition(node.start + node.length, lines),
{
format: 'plaintext',
lazyEdit: false,
snippet: false,
// we don't care about deprecations here so make sure they don't get evaluated unnecessarily
// (we don't call resolveCompletionItem)
checkDeprecatedWhenResolving: true,
useTypingExtensions: workspace.useTypingExtensions,
},
token,
true
);
}
}
40 changes: 20 additions & 20 deletions packages/pyright-internal/src/languageService/completionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,26 @@ export class CompletionProvider {
}
}

createAutoImporter(completionMap: CompletionMap, lazyEdit: boolean) {
const currentFile = this.program.getSourceFileInfo(this.fileUri);
const moduleSymbolMap = buildModuleSymbolsMap(
this.program,
this.program.getSourceFileInfoList().filter((s) => s !== currentFile)
);

return new AutoImporter(
this.program,
this.execEnv,
this.parseResults,
this.position,
completionMap,
moduleSymbolMap,
{
lazyEdit,
}
);
}

protected get evaluator() {
return this.program.evaluator!;
}
Expand Down Expand Up @@ -922,26 +942,6 @@ export class CompletionProvider {
return completionMap;
}

protected createAutoImporter(completionMap: CompletionMap, lazyEdit: boolean) {
const currentFile = this.program.getSourceFileInfo(this.fileUri);
const moduleSymbolMap = buildModuleSymbolsMap(
this.program,
this.program.getSourceFileInfoList().filter((s) => s !== currentFile)
);

return new AutoImporter(
this.program,
this.execEnv,
this.parseResults,
this.position,
completionMap,
moduleSymbolMap,
{
lazyEdit,
}
);
}

protected addAutoImportCompletions(
priorWord: string,
similarityLimit: number,
Expand Down
Loading