Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c0633c6
First cut of TS7 context
dbaeumer Jul 14, 2026
2d7b436
WIP
dbaeumer Jul 15, 2026
d94468d
Bind events correctly
dbaeumer Jul 15, 2026
02e50a1
Merge branch 'main' into dbaeumer/ts7context
dbaeumer Jul 16, 2026
cbac046
First TS7 implementation.
dbaeumer Jul 16, 2026
95a300a
First cut of TS7 context providers
dbaeumer Jul 16, 2026
b0f9c13
Minor improvements
dbaeumer Jul 17, 2026
471be7b
Merge branch 'main' into dbaeumer/ts7context
dbaeumer Aug 12, 2026
69a660e
Merge branch 'main' into dbaeumer/ts7context
dbaeumer Aug 13, 2026
cdaa25e
Bug fixes for TS7 context provider
dbaeumer Aug 14, 2026
9e69bd2
Merge branch 'main' into dbaeumer/ts7context
dbaeumer Aug 14, 2026
340a6c2
Add setting to enable TS7 language context
dbaeumer Aug 14, 2026
c2d04aa
Use getSymbolsInScope
dbaeumer Aug 18, 2026
828f1d3
Implement nes rename on TS7 Api
dbaeumer Aug 18, 2026
5a56492
First cut of code review
dbaeumer Aug 18, 2026
9b42e78
Add the TS7 enablement settings
dbaeumer Aug 19, 2026
bf3a23a
Merge branch 'main' into dbaeumer/ts7context
dbaeumer Aug 19, 2026
f4d1665
Some bug fixes
dbaeumer Aug 19, 2026
8507d55
Some final fixes
dbaeumer Aug 19, 2026
6851965
Make the shared API work
dbaeumer Aug 19, 2026
f2c0965
Reject nes rename on symbols from libs
dbaeumer Aug 20, 2026
30cce93
Consider typescriptteam.vscode-typescript extension id
dbaeumer Aug 20, 2026
e063c68
Move tests to test folders
dbaeumer Aug 20, 2026
e2c6f0f
Fix registration code.
dbaeumer Aug 20, 2026
b1ba03e
Handle cancellation correctly
dbaeumer Aug 20, 2026
21a99b3
Fix test case and awaits
dbaeumer Aug 20, 2026
648cbf6
Make inflight request fail save
dbaeumer Aug 20, 2026
9989efa
Implement correct dispose
dbaeumer Aug 20, 2026
c53f651
Merge branch 'main' into dbaeumer/ts7context
dbaeumer Aug 20, 2026
e4cb3c9
Fix failing test
dbaeumer Aug 20, 2026
4104431
Potential fix for pull request finding
dbaeumer Aug 20, 2026
fc07285
Use 7.0.2 again
dbaeumer Aug 21, 2026
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
13 changes: 12 additions & 1 deletion extensions/copilot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3879,7 +3879,18 @@
"default": true
}
},
"github.copilot.chat.languageContext.typescript.items": {
"github.copilot.chat.languageContext.typescript7.enabled": {
"type": "boolean",
"default": false,
"scope": "resource",
Comment thread
dbaeumer marked this conversation as resolved.
"tags": [
"experimental"
],
"markdownDescription": "%github.copilot.chat.languageContext.typescript7.enabled%",
"agentsWindow": {
"default": false
}
}, "github.copilot.chat.languageContext.typescript.items": {
"type": "string",
"enum": [
"minimal",
Expand Down
1 change: 1 addition & 0 deletions extensions/copilot/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@
"github.copilot.walkthrough.sparkle.media.altText": "The video shows the sparkle icon in the source control input box being clicked, triggering GitHub Copilot to generate a commit message automatically",
"github.copilot.chat.completionContext.typescript.mode": "The execution mode of the TypeScript Copilot context provider.",
"github.copilot.chat.languageContext.typescript.enabled": "Enables the TypeScript language context provider for inline suggestions",
"github.copilot.chat.languageContext.typescript7.enabled": "Enables the TypeScript language context provider for inline suggestions when using TS7 language services",
"github.copilot.chat.languageContext.typescript.items": "Controls which kind of items are included in the TypeScript language context provider.",
"github.copilot.chat.languageContext.typescript.includeDocumentation": "Controls whether to include documentation comments in the generated code snippets.",
"github.copilot.chat.languageContext.typescript.cacheTimeout": "The cache population timeout for the TypeScript language context provider in milliseconds. The default is 500 milliseconds.",
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import assert from 'node:assert';

import type * as vscode from 'vscode';
import { suite, test, vi } from 'vitest';

vi.mock('vscode', () => ({}));

import { TypeScript } from '../tsService';

suite('TypeScript service', () => {
test('prefers the current TS7 extension and falls back to the legacy extension', () => {
const currentExtensionId = 'typescriptteam.vscode-typescript';
const legacyExtensionId = 'typescriptteam.native-preview';
const scenarios = [
[currentExtensionId, legacyExtensionId],
[legacyExtensionId],
[],
];

const actual = scenarios.map(extensionIds => {
const available = new Map<string, vscode.Extension<unknown>>();
for (const extensionId of extensionIds) {
available.set(extensionId, { id: extensionId } as vscode.Extension<unknown>);
}
const lookups: string[] = [];
const extension = TypeScript.getVersion7Extension(extensionId => {
lookups.push(extensionId);
return available.get(extensionId);
});
return { selected: extension?.id, lookups };
});

assert.deepStrictEqual(actual, [
{ selected: currentExtensionId, lookups: [currentExtensionId] },
{ selected: legacyExtensionId, lookups: [currentExtensionId, legacyExtensionId] },
{ selected: undefined, lookups: [currentExtensionId, legacyExtensionId] },
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { API, Project, Snapshot, DocumentIdentifier } from '@typescript/native/unstable/async';
import {
SyntaxKind,
isArrowFunction,
isClassDeclaration,
isConstructorDeclaration,
isFunctionDeclaration,
isFunctionExpression,
isGetAccessorDeclaration,
isMethodDeclaration,
isModuleDeclaration,
isSetAccessorDeclaration,
isSourceFile,
type Node,
type SourceFile,
} from '@typescript/native/unstable/ast';
import * as protocol from '../../common/serverProtocol';
import { CompilerOptionsRunnable } from './baseContextProviders';
import { ClassContextProvider } from './classContextProvider';
import { ContextProvider, ContextRunnableCollector, type ComputeContextSession, type ContextProviderFactory, type ContextResult, type ContextRunnable, type ProviderComputeContext, type RequestContext } from './contextProvider';
import { FunctionContextProvider } from './functionContextProvider';
import { AccessorProvider, ConstructorContextProvider, MethodContextProvider } from './methodContextProvider';
import { ModuleContextProvider } from './moduleContextProvider';
import { PrepareNesRenameResult, validateNesRename } from './nesRenameValidator';
import { SourceFileContextProvider } from './sourceFileContextProvider';
import { RecoverableError } from './types';
import tss, { Symbols, type CancellationTokenWithTimer } from './typescripts';

class ProviderComputeContextImpl implements ProviderComputeContext {
private firstCallableProvider: ContextProvider | undefined;

public update(contextProvider: ContextProvider): ContextProvider {
if (this.firstCallableProvider === undefined && contextProvider.isCallableProvider === true) {
this.firstCallableProvider = contextProvider;
}
return contextProvider;
}

public isFirstCallableProvider(contextProvider: ContextProvider): boolean {
return this.firstCallableProvider === contextProvider;
}
}

class ContextProviders {
private static readonly Factories = new Map<SyntaxKind, ContextProviderFactory>([
[SyntaxKind.SourceFile, (_node, tokenInfo, computeContext) => new SourceFileContextProvider(tokenInfo, computeContext)],
[SyntaxKind.FunctionDeclaration, (node, tokenInfo, computeContext) => isFunctionDeclaration(node) ? new FunctionContextProvider(node, tokenInfo, computeContext) : undefined],
[SyntaxKind.ArrowFunction, (node, tokenInfo, computeContext) => isArrowFunction(node) ? new FunctionContextProvider(node, tokenInfo, computeContext) : undefined],
[SyntaxKind.FunctionExpression, (node, tokenInfo, computeContext) => isFunctionExpression(node) ? new FunctionContextProvider(node, tokenInfo, computeContext) : undefined],
[SyntaxKind.GetAccessor, (node, tokenInfo, computeContext) => isGetAccessorDeclaration(node) ? new AccessorProvider(node, tokenInfo, computeContext) : undefined],
[SyntaxKind.SetAccessor, (node, tokenInfo, computeContext) => isSetAccessorDeclaration(node) ? new AccessorProvider(node, tokenInfo, computeContext) : undefined],
[SyntaxKind.ClassDeclaration, (node, tokenInfo) => isClassDeclaration(node) ? ClassContextProvider.create(node, tokenInfo) : undefined],
[SyntaxKind.Constructor, (node, tokenInfo, computeContext) => isConstructorDeclaration(node) ? new ConstructorContextProvider(node, tokenInfo, computeContext) : undefined],
[SyntaxKind.MethodDeclaration, (node, tokenInfo, computeContext) => isMethodDeclaration(node) ? new MethodContextProvider(node, tokenInfo, computeContext) : undefined],
[SyntaxKind.ModuleDeclaration, (node, tokenInfo, computeContext) => isModuleDeclaration(node) ? new ModuleContextProvider(node, tokenInfo, computeContext) : undefined],
]);

private readonly tokenInfo: tss.TokenInfo;
private readonly computeInfo: ProviderComputeContextImpl = new ProviderComputeContextImpl();

constructor(tokenInfo: tss.TokenInfo) {
this.tokenInfo = tokenInfo;
}

public async execute(result: ContextResult, session: ComputeContextSession, project: Project, token: CancellationTokenWithTimer): Promise<void> {
const collector = await this.getContextRunnables(session, project, result.context, token);
result.addPath(tss.StableSyntaxKinds.getPath(this.tokenInfo.touching ?? this.tokenInfo.token));
for (const runnable of collector.entries()) {
runnable.initialize(result);
}
await this.executeRunnables(collector.getPrimaryRunnables(), result, token);
await this.executeRunnables(collector.getSecondaryRunnables(), result, token);
await this.executeRunnables(collector.getTertiaryRunnables(), result, token);
result.done();
}

private async executeRunnables(runnables: ContextRunnable[], result: ContextResult, token: CancellationTokenWithTimer): Promise<void> {
for (const runnable of runnables) {
token.throwIfCancellationRequested();
try {
await runnable.compute(token);
} catch (error) {
if (error instanceof RecoverableError) {
result.addErrorData(error);
} else {
throw error;
}
}
}
}

private async getContextRunnables(session: ComputeContextSession, project: Project, context: RequestContext, token: CancellationTokenWithTimer): Promise<ContextRunnableCollector> {
const result = new ContextRunnableCollector(context.clientSideRunnableResults);
result.addPrimary(new CompilerOptionsRunnable(session, project, context, this.tokenInfo.token.getSourceFile()));
for (const provider of this.computeProviders()) {
await provider.provide(result, session, project, context, token);
}
return result;
}

private computeProviders(): ContextProvider[] {
const result: ContextProvider[] = [];
let token: Node | undefined = this.tokenInfo.touching;
if (token === undefined) {
token = this.tokenInfo.token.kind === SyntaxKind.EndOfFile ? this.tokenInfo.previous : this.tokenInfo.token;
}
if (token === undefined || token.kind === SyntaxKind.EndOfFile) {
return result;
}
let current: Node | undefined = token;
while (current !== undefined) {
const factory = ContextProviders.Factories.get(current.kind);
const provider = factory?.(current, this.tokenInfo, this.computeInfo);
if (provider !== undefined) {
result.push(this.computeInfo.update(provider));
}
if (isSourceFile(current)) {
break;
}
current = current.parent;
}
return result;
}
}

export async function computeContext(result: ContextResult, session: ComputeContextSession, project: Project, document: SourceFile, position: number, token: CancellationTokenWithTimer): Promise<void> {
const sourceFile = await project.program.getSourceFile(document.fileName);
if (sourceFile === undefined) {
result.addErrorData(new RecoverableError('No source file found for document', RecoverableError.NoSourceFile));
return;
}
const tokenInfo = tss.getRelevantTokens(sourceFile, position);
await new ContextProviders(tokenInfo).execute(result, session, project, token);
}

export async function prepareNesRename<FromLSP extends boolean>(result: PrepareNesRenameResult, api: API<FromLSP>, snapshot: Snapshot, project: Project, document: SourceFile, position: number, oldName: string | undefined, newName: string | undefined, lastSymbolRename: protocol.Range | undefined, token: CancellationTokenWithTimer): Promise<void> {
if (typeof oldName !== 'string' || oldName.length === 0) {
result.setCanRename(protocol.RenameKind.no, 'No old name provided');
return;
}
if (typeof newName !== 'string' || newName.length === 0) {
result.setCanRename(protocol.RenameKind.no, 'No new name provided');
return;
}

const state = await doPrepareNesRename(result, project, document, position, oldName, newName, token);
if (state !== PrepareState.unavailable || lastSymbolRename === undefined) {
return;
}

const [oldText, oldPosition] = getOldText(document, position, oldName, newName, lastSymbolRename);
await runWithTemporaryFileUpdate<FromLSP>(api, snapshot, document.fileName, oldText, async updatedSnapshot => {
const updatedProject = await getUpdatedProject(updatedSnapshot, project, document.fileName);
const updatedSourceFile = await updatedProject?.program.getSourceFile(document.fileName);
if (updatedProject === undefined || updatedSourceFile === undefined) {
result.setCanRename(protocol.RenameKind.no, 'No source file found for document');
return;
}
const updatedState = await doPrepareNesRename(result, updatedProject, updatedSourceFile, oldPosition, oldName, newName, token);
if (updatedState === PrepareState.prepared && (result.getCanRename() === protocol.RenameKind.maybe || result.getCanRename() === protocol.RenameKind.yes)) {
result.setOnOldState(true);
}
});
}

export async function nesRename<FromLSP extends boolean>(api: API<FromLSP>, snapshot: Snapshot, project: Project, document: SourceFile, position: number, oldName: string | undefined, newName: string | undefined, lastSymbolRename: protocol.Range | undefined, token: CancellationTokenWithTimer): Promise<protocol.RenameGroup[]> {
if (oldName === undefined || newName === undefined || lastSymbolRename === undefined) {
return [];
}

const [oldText, oldPosition] = getOldText(document, position, oldName, newName, lastSymbolRename);
const groups = new Map<string, protocol.RenameGroup>();
const seen = new Set<string>();
await runWithTemporaryFileUpdate<FromLSP>(api, snapshot, document.fileName, oldText, async updatedSnapshot => {
const updatedProject = await getUpdatedProject(updatedSnapshot, project, document.fileName);
const updatedSourceFile = await updatedProject?.program.getSourceFile(document.fileName);
if (updatedProject === undefined || updatedSourceFile === undefined) {
return;
}
const renameTarget = getRenameTarget(updatedSourceFile, oldPosition, oldName);
if (renameTarget.node.getText(updatedSourceFile) !== oldName) {
return;
}
const symbols = new Symbols(updatedProject, token);
const referencedSymbols = await updatedProject.checker.getReferencedSymbolsForNode(renameTarget.node, renameTarget.position);
for (const referencedSymbol of referencedSymbols) {
const definition = await referencedSymbol.definition.resolve(updatedProject);
if (definition === undefined || await symbols.isSourceFileFromLibrary(definition.getSourceFile())) {
return;
}
}
for (const referencedSymbol of referencedSymbols) {
for (const reference of referencedSymbol.references) {
token.throwIfCancellationRequested();
const node = await reference.resolve(updatedProject);
if (node === undefined) {
continue;
}
const sourceFile = node.getSourceFile();
if (await symbols.isSourceFileFromLibrary(sourceFile)) {
continue;
}
const startPosition = node.getStart(sourceFile);
Comment thread
dbaeumer marked this conversation as resolved.
const endPosition = node.getEnd();
const key = `${sourceFile.path}:${startPosition}:${endPosition}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
const start = sourceFile.getLineAndCharacterOfPosition(startPosition);
const end = sourceFile.getLineAndCharacterOfPosition(endPosition);
const delta = newName.length - oldName.length;
if (
sourceFile.fileName === document.fileName &&
start.line === lastSymbolRename.start.line && start.character === lastSymbolRename.start.character &&
end.line === lastSymbolRename.end.line && end.character === lastSymbolRename.end.character - delta
) {
continue;
}
let group = groups.get(sourceFile.fileName);
if (group === undefined) {
group = { file: sourceFile.fileName, changes: [] };
groups.set(sourceFile.fileName, group);
}
group.changes.push({
range: {
start: { line: start.line, character: start.character },
end: { line: end.line, character: end.character },
},
});
}
}
});
return Array.from(groups.values());
}

function runWithTemporaryFileUpdate<FromLSP extends boolean>(api: API<FromLSP>, baseSnapshot: Snapshot, file: DocumentIdentifier, newText: string, cb: (newSnapshot: Snapshot) => void | Promise<void>): Promise<void> {
interface ApiWithTemporaryFileUpdate {
runWithTemporaryFileUpdate(baseSnapshot: Snapshot, file: DocumentIdentifier, newText: string, cb: (newSnapshot: Snapshot) => void | Promise<void>): Promise<void>;
}
if (typeof (api as unknown as ApiWithTemporaryFileUpdate).runWithTemporaryFileUpdate === 'function') {
return (api as unknown as ApiWithTemporaryFileUpdate).runWithTemporaryFileUpdate(baseSnapshot, file, newText, cb);
}
return Promise.resolve();
}

const enum PrepareState {
prepared,
unavailable,
mismatch,
}

async function doPrepareNesRename(result: PrepareNesRenameResult, project: Project, sourceFile: SourceFile, position: number, oldName: string, newName: string, token: CancellationTokenWithTimer): Promise<PrepareState> {
const renameTarget = getRenameTarget(sourceFile, position, oldName);
const tokenText = renameTarget.node.getText(sourceFile);
if (tokenText !== oldName) {
result.setCanRename(protocol.RenameKind.no, `Old name '${oldName}' does not match symbol name '${tokenText}'`);
return PrepareState.mismatch;
}
token.throwIfCancellationRequested();
if (await project.checker.getSymbolAtLocation(renameTarget.node) === undefined) {
result.setCanRename(protocol.RenameKind.no, 'No symbol found at location');
return PrepareState.unavailable;
}
result.setCanRename(protocol.RenameKind.maybe, oldName);
await validateNesRename(result, project, renameTarget.node, oldName, newName, token);
return PrepareState.prepared;
}

function getRenameTarget(sourceFile: SourceFile, position: number, oldName: string): { node: Node; position: number } {
const token = tss.getRelevantTokens(sourceFile, position).token;
if (token.getText(sourceFile) === oldName) {
return { node: token, position };
}
let current: Node | undefined = token.parent;
while (current !== undefined && !isSourceFile(current)) {
if (isFunctionDeclaration(current) && current.name?.getText(sourceFile) === oldName) {
return { node: current.name, position: current.name.getStart(sourceFile) };
}
current = current.parent;
}
return { node: token, position };
}

async function getUpdatedProject(snapshot: Snapshot, project: Project, fileName: string): Promise<Project | undefined> {
return snapshot.getProject(project.configFileName) ?? await snapshot.getDefaultProjectForFile(fileName);
}

function getOldText(sourceFile: SourceFile, position: number, oldName: string, newName: string, lastSymbolRename: protocol.Range): [string, number] {
const startPosition = sourceFile.getPositionOfLineAndCharacter(lastSymbolRename.start.line, lastSymbolRename.start.character);
const endPosition = sourceFile.getPositionOfLineAndCharacter(lastSymbolRename.end.line, lastSymbolRename.end.character);
const oldText = sourceFile.text.substring(0, startPosition) + oldName + sourceFile.text.substring(endPosition);
return [oldText, position < startPosition ? position : position - (newName.length - oldName.length)];
}
Loading
Loading