Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
252 changes: 152 additions & 100 deletions packages/pyright-internal/src/languageService/codeActionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,24 @@ 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 { 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 { FunctionNode, ParseNode, ParseNodeType } from '../parser/parseNodes';
import { sorter } from '../common/collectionUtils';
import { LanguageServerInterface } from '../common/languageServerInterface';
import { DiagnosticRule } from '../common/diagnosticRules';
import { TextRangeCollection } from '../common/textRangeCollection';

export class CodeActionProvider {
static mightSupport(kinds: CodeActionKind[] | undefined): boolean {
Expand Down Expand Up @@ -64,117 +66,139 @@ 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 [];
const fs = ls.serviceProvider.fs();
for (const diagnostic of diags) {
const rule = diagnostic.getRule();
if (!rule) {
continue;
}
const line = diagnostic.range.start.line;

const node = findNodeByOffset(parseResults.parserOutput.parseTree, offset);
if (node === undefined) {
return [];
}
// ==== CA for auto imports ====
if (diagnostic.getActions()?.some((action) => action.action === Commands.import)) {
const offset = convertPositionToOffset(range.start, lines);
if (offset === undefined) {
return [];
}

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
);
const node = findNodeByOffset(parseResults.parserOutput.parseTree, offset);
if (node === 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 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 adding @override decorators ====
if (rule === DiagnosticRule.reportImplicitOverride) {
const lineText = lines.getItemAt(line);
const methodLineContent = parseResults.text.substring(lineText.start, lineText.start + lineText.length);

const functionLine = { line: line, character: 0 };
const offset = convertPositionToOffset(functionLine, lines);
if (offset === undefined) {
return [];
}
if (textEdits.length === 0) {
continue;

const node = findNodeByOffset(parseResults.parserOutput.parseTree, offset);
if (node === undefined) {
return [];
}
const workspaceEdit = convertToWorkspaceEdit(
ls.convertUriToLspUriString,
completer.importResolver.fileSystem,
convertToFileTextEdits(fileUri, convertToTextEditActions(textEdits))
);
codeActions.push(
CodeAction.create(
suggestedImport.data.autoImportText.trim(),
workspaceEdit,
CodeActionKind.QuickFix
)
);
}
}

if (!workspace.rootUri) {
return codeActions;
}
const completer = this._createCompleter(workspace, fileUri, token, node, lines);
const completionMap = new CompletionMap();

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
const overrideEdits = completer.createOverrideEdits(
node as FunctionNode,
offset,
undefined,
methodLineContent,
completionMap
);
codeActions.push(createTypeStubAction);

if (overrideEdits.length > 0) {
codeActions.push(
CodeAction.create(
Localizer.CodeAction.addExplicitOverride(),
convertToWorkspaceEdit(
ls.convertUriToLspUriString,
fs,
convertToFileTextEdits(fileUri, overrideEdits)
),
CodeActionKind.QuickFix
)
);
}
}
}

const fs = ls.serviceProvider.fs();
for (const diagnostic of diags) {
const rule = diagnostic.getRule();
if (!rule) {
continue;
// ==== CA for creating type stubs ====
if (workspace.rootUri && 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);
}
}

// ==== 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 +213,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 +244,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
);
}
}
Loading