Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5e72826
AI quickFix noImplicitAny: replaces inferFromUsage
sandersn Jul 6, 2023
44375f7
Offer infer-types quickfix separate from Typescript one
sandersn Aug 14, 2023
a288cbb
Merge branch 'main' into ai-inferFromUsage
sandersn Aug 14, 2023
4cb0bc5
Clean up for PR
sandersn Aug 17, 2023
81fad69
AI name suggestions in Extract Constant refactor
sandersn Aug 22, 2023
d8753fb
Merge branch 'main' into ai-codefixes
sandersn Aug 23, 2023
ea5af2d
Include selected expression in conversatin prompt
sandersn Aug 23, 2023
2267ba8
Support extract to function and type alias as well
sandersn Aug 23, 2023
2aaf53b
Support addNameToNamelessParameter, refactor copilot code
sandersn Aug 25, 2023
63c84d3
Rest of useful codefixes
sandersn Aug 31, 2023
a0a2cb4
Pretty good ranges based on RefactorInfo
sandersn Sep 6, 2023
9c5b16a
Switch entirely to inline+action-based expansion
sandersn Sep 7, 2023
7906b2b
Merge branch 'main' into ai-codefixes
sandersn Sep 7, 2023
3328d54
Clean up for review
sandersn Sep 8, 2023
b1b3506
Fix lints
sandersn Sep 14, 2023
d1d6055
Add more complex options
sandersn Sep 14, 2023
968d1bd
Merge branch 'main' into ai-codefixes
sandersn Sep 14, 2023
ba511b3
Merge branch 'main' into ai-codefixes
sandersn Sep 20, 2023
530870b
Config: change defaults and add descriptions
sandersn Sep 20, 2023
6c154dd
Mark quickfixes/refactors with Copilot: prefix
sandersn Sep 20, 2023
1e7d82c
Code correctly treats empty config as default-false
sandersn Sep 21, 2023
32bb215
Merge remote-tracking branch 'origin/main' into ai-codefixes
sandersn Sep 21, 2023
a9a66e4
Correct+customise copilot-specific titles
sandersn Sep 21, 2023
d534d6b
Tweak wording and position of copilot infer-types
sandersn Sep 21, 2023
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
57 changes: 52 additions & 5 deletions extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,58 @@
"title": "%configuration.typescript%",
"order": 20,
"properties": {
"typescript.experimental.aiQuickFix": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiQuickFix%",
"scope": "resource"
"typescript.experimental.aiCodeActions": {
"type": "object",
"default": {},
"description": "%typescript.experimental.aiCodeActions%",
"scope": "resource",
"properties": {
"classIncorrectlyImplementsInterface": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.classIncorrectlyImplementsInterface%"
},
"classDoesntImplementInheritedAbstractMember": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.classDoesntImplementInheritedAbstractMember%"
},
"missingFunctionDeclaration": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.missingFunctionDeclaration%"
},
"inferAndAddTypes": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.inferAndAddTypes%"
},
"addNameToNamelessParameter": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.addNameToNamelessParameter%"
},
"extractConstant": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.extractConstant%"
},
"extractFunction": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.extractFunction%"
},
"extractType": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.extractType%"
},
"extractInterface": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.extractInterface%"
}
}
},
"typescript.tsdk": {
"type": "string",
Expand Down
11 changes: 10 additions & 1 deletion extensions/typescript-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@
"configuration.suggest.completeFunctionCalls": "Complete functions with their parameter signature.",
"configuration.suggest.includeAutomaticOptionalChainCompletions": "Enable/disable showing completions on potentially undefined values that insert an optional chain call. Requires strict null checks to be enabled.",
"configuration.suggest.includeCompletionsForImportStatements": "Enable/disable auto-import-style completions on partially-typed import statements.",
"typescript.experimental.aiQuickFix": "Enable/disable AI-assisted quick fixes. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions": "Enable/disable AI-assisted code actions. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.classIncorrectlyImplementsInterface": "Enable/disable AI assistance for Class Incorrectly Implements Interface quickfix. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.classDoesntImplementInheritedAbstractMember": "Enable/disable AI assistance for Class Doesn't Implement Inherited Abstract Member quickfix. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.missingFunctionDeclaration": "Enable/disable AI assistance for Missing Function Declaration quickfix. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.inferAndAddTypes": "Enable/disable AI assistance for Infer and Add Types refactor. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.addNameToNamelessParameter": "Enable/disable AI assistance for Add Name to Nameless Parameter quickfix. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.extractConstant": "Enable/disable AI assistance for Extract Constant refactor. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.extractFunction": "Enable/disable AI assistance for Extract Function refactor. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.extractType": "Enable/disable AI assistance for Extract Type refactor. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.extractInterface": "Enable/disable AI assistance for Extract Interface refactor. Requires an extension providing AI chat functionality.",
"typescript.tsdk.desc": "Specifies the folder path to the tsserver and `lib*.d.ts` files under a TypeScript install to use for IntelliSense, for example: `./node_modules/typescript/lib`.\n\n- When specified as a user setting, the TypeScript version from `typescript.tsdk` automatically replaces the built-in TypeScript version.\n- When specified as a workspace setting, `typescript.tsdk` allows you to switch to use that workspace version of TypeScript for IntelliSense with the `TypeScript: Select TypeScript version` command.\n\nSee the [TypeScript documentation](https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-newer-typescript-versions) for more detail about managing TypeScript versions.",
"typescript.disableAutomaticTypeAcquisition": "Disables [automatic type acquisition](https://code.visualstudio.com/docs/nodejs/working-with-javascript#_typings-and-automatic-type-acquisition). Automatic type acquisition fetches `@types` packages from npm to improve IntelliSense for external libraries.",
"typescript.enablePromptUseWorkspaceTsdk": "Enables prompting of users to use the TypeScript version configured in the workspace for Intellisense.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { DiagnosticsManager } from './diagnostics';
import FileConfigurationManager from './fileConfigurationManager';
import { applyCodeActionCommands, getEditForCodeAction } from './util/codeAction';
import { conditionalRegistration, requireSomeCapability } from './util/dependentRegistration';
import { Expand, EditorChatFollowUp, CompositeCommand } from './util/copilot';

type ApplyCodeActionCommand_args = {
readonly document: vscode.TextDocument;
Expand All @@ -26,42 +27,6 @@ type ApplyCodeActionCommand_args = {
readonly followupAction?: Command;
};

class EditorChatFollowUp implements Command {

id: string = '_typescript.quickFix.editorChatFollowUp';

constructor(private readonly prompt: string, private readonly document: vscode.TextDocument, private readonly range: vscode.Range, private readonly client: ITypeScriptServiceClient) {
}

async execute() {
const findScopeEndLineFromNavTree = (startLine: number, navigationTree: Proto.NavigationTree[]): vscode.Range | undefined => {
for (const node of navigationTree) {
const range = typeConverters.Range.fromTextSpan(node.spans[0]);
if (startLine === range.start.line) {
return range;
} else if (startLine > range.start.line && startLine <= range.end.line && node.childItems) {
return findScopeEndLineFromNavTree(startLine, node.childItems);
}
}
return undefined;
};
const filepath = this.client.toOpenTsFilePath(this.document);
if (!filepath) {
return;
}
const response = await this.client.execute('navtree', { file: filepath }, nulToken);
if (response.type !== 'response' || !response.body?.childItems) {
return;
}
const startLine = this.range.start.line;
const enclosingRange = findScopeEndLineFromNavTree(startLine, response.body.childItems);
if (!enclosingRange) {
return;
}
await vscode.commands.executeCommand('vscode.editorChat.start', { initialRange: enclosingRange, message: this.prompt, autoSend: true });
}
}

class ApplyCodeActionCommand implements Command {
public static readonly ID = '_typescript.applyCodeActionCommand';
public readonly id = ApplyCodeActionCommand.ID;
Expand Down Expand Up @@ -255,8 +220,10 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
private readonly diagnosticsManager: DiagnosticsManager,
telemetryReporter: TelemetryReporter
) {
commandManager.register(new CompositeCommand());
commandManager.register(new ApplyCodeActionCommand(client, diagnosticsManager, telemetryReporter));
commandManager.register(new ApplyFixAllCodeAction(client, telemetryReporter));
commandManager.register(new EditorChatFollowUp(client));

this.supportedCodeActionProvider = new SupportedCodeActionProvider(client);
}
Expand Down Expand Up @@ -340,42 +307,87 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
}

for (const tsCodeFix of response.body) {
this.addAllFixesForTsCodeAction(results, document, file, diagnostic, tsCodeFix as Proto.CodeFixAction);
for (const action of this.getFixesForTsCodeAction(document, diagnostic, tsCodeFix)) {
results.addAction(action);
}
this.addFixAllForTsCodeAction(results, document.uri, file, diagnostic, tsCodeFix as Proto.CodeFixAction);
}
return results;
}

private addAllFixesForTsCodeAction(
results: CodeActionSet,
private getFixesForTsCodeAction(
document: vscode.TextDocument,
file: string,
diagnostic: vscode.Diagnostic,
tsAction: Proto.CodeFixAction
): CodeActionSet {
results.addAction(this.getSingleFixForTsCodeAction(document, diagnostic, tsAction));
this.addFixAllForTsCodeAction(results, document.uri, file, diagnostic, tsAction as Proto.CodeFixAction);
return results;
}

private getSingleFixForTsCodeAction(
document: vscode.TextDocument,
diagnostic: vscode.Diagnostic,
tsAction: Proto.CodeFixAction
): VsCodeCodeAction {
const aiQuickFixEnabled = vscode.workspace.getConfiguration('typescript').get('experimental.aiQuickFix');
let followupAction: Command | undefined;
if (aiQuickFixEnabled && tsAction.fixName === fixNames.classIncorrectlyImplementsInterface) {
followupAction = new EditorChatFollowUp('Implement the class using the interface', document, diagnostic.range, this.client);
}
const codeAction = new VsCodeCodeAction(tsAction, tsAction.description, vscode.CodeActionKind.QuickFix);
codeAction.edit = getEditForCodeAction(this.client, tsAction);
action: Proto.CodeFixAction
): VsCodeCodeAction[] {
const actions: VsCodeCodeAction[] = [];
const codeAction = new VsCodeCodeAction(action, action.description, vscode.CodeActionKind.QuickFix);
codeAction.edit = getEditForCodeAction(this.client, action);
codeAction.diagnostics = [diagnostic];
codeAction.command = {
command: ApplyCodeActionCommand.ID,
arguments: [<ApplyCodeActionCommand_args>{ action: tsAction, diagnostic, document, followupAction }],
arguments: [<ApplyCodeActionCommand_args>{ action: action, diagnostic, document }],
title: ''
};
return codeAction;
actions.push(codeAction);

if (vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions')) {
let message: string | undefined;
let expand: Expand | undefined;

if (action.fixName === fixNames.classIncorrectlyImplementsInterface && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.classIncorrectlyImplementsInterface') !== false) {
message = `Implement the stubbed-out class members for ${document.getText(diagnostic.range)} with a useful implementation.`;
expand = { kind: 'code-action', action };
}
else if (action.fixName === fixNames.fixClassDoesntImplementInheritedAbstractMember && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.classDoesntImplementInheritedAbstractMember') !== false) {
message = `Implement the stubbed-out class members for ${document.getText(diagnostic.range)} with a useful implementation.`;
expand = { kind: 'code-action', action };
}
else if (action.fixName === fixNames.fixMissingFunctionDeclaration && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.missingFunctionDeclaration') !== false) {
message = `Provide a reasonable implementation of the function ${document.getText(diagnostic.range)} given its type and the context it's called in.`;
expand = { kind: 'code-action', action };
}
else if (action.fixName === fixNames.inferFromUsage && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.inferAndAddTypes') !== false) {
const inferFromBody = new VsCodeCodeAction(action, 'Copilot: Infer and add types', vscode.CodeActionKind.QuickFix);
inferFromBody.edit = new vscode.WorkspaceEdit();
inferFromBody.diagnostics = [diagnostic];
inferFromBody.command = {
command: EditorChatFollowUp.ID,
arguments: [<EditorChatFollowUp.Args>{
message: 'Add types to this code. Add separate interfaces when possible. Do not change the code except for adding types.',
expand: { kind: 'navtree-function', pos: diagnostic.range.start },
document
}],
title: ''
};
actions.push(inferFromBody);
}
else if (action.fixName === fixNames.addNameToNamelessParameter && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.addNameToNamelessParameter') !== false) {
const newText = action.changes.map(change => change.textChanges.map(textChange => textChange.newText).join('')).join('');
message = `Rename the parameter ${newText} with a more meaningful name.`;
expand = {
kind: 'navtree-function',
pos: diagnostic.range.start
};
}
if (expand && message !== undefined) {
codeAction.command.title = 'Copilot: ' + codeAction.command.title;
codeAction.command = {
command: CompositeCommand.ID,
title: '',
arguments: [codeAction.command, {
command: EditorChatFollowUp.ID,
title: '',
arguments: [<EditorChatFollowUp.Args>{
message,
expand,
document
}],
}],
};
}
}
return actions;
}

private addFixAllForTsCodeAction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { coalesce } from '../utils/arrays';
import { nulToken } from '../utils/cancellation';
import FormattingOptionsManager from './fileConfigurationManager';
import { conditionalRegistration, requireSomeCapability } from './util/dependentRegistration';
import { EditorChatFollowUp, CompositeCommand } from './util/copilot';

function toWorkspaceEdit(client: ITypeScriptServiceClient, edits: readonly Proto.FileCodeEdits[]): vscode.WorkspaceEdit {
const workspaceEdit = new vscode.WorkspaceEdit();
Expand All @@ -34,17 +35,6 @@ function toWorkspaceEdit(client: ITypeScriptServiceClient, edits: readonly Proto
}


class CompositeCommand implements Command {
public static readonly ID = '_typescript.compositeCommand';
public readonly id = CompositeCommand.ID;

public async execute(...commands: vscode.Command[]): Promise<void> {
for (const command of commands) {
await vscode.commands.executeCommand(command.command, ...(command.arguments ?? []));
}
}
}

namespace DidApplyRefactoringCommand {
export interface Args {
readonly action: string;
Expand Down Expand Up @@ -355,6 +345,7 @@ class InlinedCodeAction extends vscode.CodeAction {
public readonly refactor: Proto.ApplicableRefactorInfo,
public readonly action: Proto.RefactorActionInfo,
public readonly range: vscode.Range,
public readonly copilotRename?: (info: Proto.RefactorEditInfo) => vscode.Command,
) {
super(action.description, InlinedCodeAction.getKind(action));

Expand Down Expand Up @@ -395,18 +386,21 @@ class InlinedCodeAction extends vscode.CodeAction {
if (response.body.renameLocation) {
// Disable renames in interactive playground https://github.com/microsoft/vscode/issues/75137
if (this.document.uri.scheme !== fileSchemes.walkThroughSnippet) {
if (this.copilotRename && this.command) {
this.command.title = 'Copilot: ' + this.command.title;
}
this.command = {
command: CompositeCommand.ID,
title: '',
arguments: coalesce([
this.command,
{
this.copilotRename ? this.copilotRename(response.body) : {
command: 'editor.action.rename',
arguments: [[
this.document.uri,
typeConverters.Position.fromLocation(response.body.renameLocation)
]]
}
},
])
};
}
Expand Down Expand Up @@ -456,7 +450,6 @@ class SelectCodeAction extends vscode.CodeAction {
};
}
}

type TsCodeAction = InlinedCodeAction | MoveToFileCodeAction | SelectCodeAction;

class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeAction> {
Expand All @@ -471,6 +464,7 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeActi
commandManager.register(new CompositeCommand());
commandManager.register(new SelectRefactorCommand(this.client));
commandManager.register(new MoveToFileRefactorCommand(this.client, didApplyRefactoringCommand));
commandManager.register(new EditorChatFollowUp(this.client));
}

public static readonly metadata: vscode.CodeActionProviderMetadata = {
Expand Down Expand Up @@ -582,7 +576,36 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeActi
if (action.name === 'Move to file') {
codeAction = new MoveToFileCodeAction(document, action, rangeOrSelection);
} else {
codeAction = new InlinedCodeAction(this.client, document, refactor, action, rangeOrSelection);
let copilotRename: ((info: Proto.RefactorEditInfo) => vscode.Command) | undefined;
if (vscode.workspace.getConfiguration('typescript', null).get('experimental.aiCodeActions')) {
if (Extract_Constant.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractConstant') !== false
|| Extract_Function.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractFunction') !== false
|| Extract_Type.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractType') !== false
|| Extract_Interface.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractInterface') !== false) {
const newName = Extract_Constant.matches(action) ? 'newLocal'
: Extract_Function.matches(action) ? 'newFunction'
: Extract_Type.matches(action) ? 'NewType'
: Extract_Interface.matches(action) ? 'NewInterface'
: '';
copilotRename = info => ({
title: '',
command: EditorChatFollowUp.ID,
arguments: [<EditorChatFollowUp.Args>{
message: `Rename ${newName} to a better name based on usage.`,
expand: Extract_Constant.matches(action) ? {
kind: 'navtree-function',
pos: typeConverters.Position.fromLocation(info.renameLocation!),
} : {
kind: 'refactor-info',
refactor: info,
},
document,
}]
});
}

}
codeAction = new InlinedCodeAction(this.client, document, refactor, action, rangeOrSelection, copilotRename);
}

codeAction.isPreferred = TypeScriptRefactorProvider.isPreferred(action, allActions);
Expand Down
Loading