Skip to content

feat(language-service): add Angular-specific Inlay Hints (LSP 3.17)#66755

Merged
atscott merged 4 commits intoangular:mainfrom
kbrilla:feat/inlay-hints
Mar 6, 2026
Merged

feat(language-service): add Angular-specific Inlay Hints (LSP 3.17)#66755
atscott merged 4 commits intoangular:mainfrom
kbrilla:feat/inlay-hints

Conversation

@kbrilla
Copy link
Copy Markdown
Contributor

@kbrilla kbrilla commented Jan 26, 2026

PR Checklist

  • The commit message follows our guidelines
  • Tests for the changes have been added
  • Docs have been added / updated

PR Type

  • Feature
  • Refactoring (small infrastructure extraction to simplify review)

What is the current behavior?

Issue Number: #66730

What is the new behavior?

This PR adds Angular-specific template inlay hints end-to-end across the language service and VS Code extension.

It also includes a small server-side workspace configuration helper refactor so inlay hint settings can be pulled and refreshed cleanly.

Highlights:

  • Angular template inlay hints for variables, bindings, events, pipes, and control flow.
  • LSP textDocument/inlayHint and inlayHint/resolve support in the extension server.
  • Angular inlay hint settings in the VS Code extension with runtime refresh behavior.
  • Request guards to avoid Angular inlay hint requests on unsupported TypeScript regions.
  • Unit and integration regression coverage.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

  • This PR no longer has an external dependency on a separate workspace-configuration PR.

Screenshots

image image image image

@pullapprove pullapprove Bot requested a review from devversion January 26, 2026 16:18
@angular-robot angular-robot Bot added detected: feature PR contains a feature commit area: language-service Issues related to Angular's VS Code language service labels Jan 26, 2026
@ngbot ngbot Bot added this to the Backlog milestone Jan 26, 2026
@kbrilla kbrilla marked this pull request as draft January 26, 2026 16:59
Comment on lines +234 to +239
const tsHints = languageService.provideInlayHints(scriptInfo.fileName, span, tsPreferences);

for (const tsHint of tsHints) {
const position = scriptInfo.positionToLineOffset(tsHint.position);
hints.push(convertTsInlayHint(tsHint, position));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my testing, this should be omitted. Typescript provides its own inlay hints and we should not duplicate them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed TS inlay hints from Angular response

Comment thread vscode-ng-language-service/package.json Outdated
"default": true,
"markdownDescription": "When enabled, the Angular Language Service will delegate file watching to VS Code instead of creating its own internal file watchers. This can significantly improve performance (greater than 10x faster initialization) and reduce resource usage in large repositories."
},
"angular.inlayHints.forLoopVariableTypes": {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all of these should be defaulted to their "off" equivalents

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I switched Angular inlay hint options to off by default. I also cleaned up config handling so toggles apply live without requiring extension restart.

Comment thread vscode-ng-language-service/package.json Outdated
"default": true,
"markdownDescription": "When enabled, the Angular Language Service will delegate file watching to VS Code instead of creating its own internal file watchers. This can significantly improve performance (greater than 10x faster initialization) and reduce resource usage in large repositories."
},
"angular.inlayHints.forLoopVariableTypes": {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have so many options, we could benefit from better grouping like typescript has

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added Inlay Hints subcategory, and grouped existing settings into Preferences subcategory. Should we split it even more? Suggestions, Diagnostics?
image

@devversion devversion removed their request for review February 11, 2026 17:29
@atscott
Copy link
Copy Markdown
Contributor

atscott commented Feb 18, 2026

@kbrilla Do you plan on continuing work on this PR?

@kbrilla
Copy link
Copy Markdown
Contributor Author

kbrilla commented Feb 20, 2026

, I was not able to look into it today, but will continue tomorrow

@angular-robot angular-robot Bot added area: docs Related to the documentation area: vscode-extension Issues related to the Angular Language Service VsCode extension labels Feb 21, 2026
@kbrilla kbrilla force-pushed the feat/inlay-hints branch 3 times, most recently from c1e6168 to abefeba Compare February 23, 2026 10:52
@kbrilla kbrilla marked this pull request as ready for review February 23, 2026 11:15
@kbrilla kbrilla requested a review from atscott February 23, 2026 11:15
@kbrilla
Copy link
Copy Markdown
Contributor Author

kbrilla commented Feb 23, 2026

Updated PR description and added screenshots

@kbrilla kbrilla force-pushed the feat/inlay-hints branch 7 times, most recently from a088340 to 4ccf175 Compare February 24, 2026 21:23
Comment thread vscode-ng-language-service/package.json Outdated
{
"name": "ng-template",
"displayName": "Angular Language Service",
"displayName": "Angular",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! PR #66690 was sharing those 2 commits, so I updated it there as well, together with other workspace settings-related changes.

// client can be deactivated on extension deactivation
registerCommands(client, context);

// Restart the server on configuration change.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this removal intentional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, though it is no longer valid, as you were right in, and a restart is still needed

registerCommands(client, context);

// Restart the server on configuration change.
const disposable = vscode.workspace.onDidChangeConfiguration(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this change should move to the next commit

);

this.disposables.push(
vscode.workspace.onDidChangeConfiguration(() => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we removing the stop/start of the language client? Don't some settings invalidate the session configuration?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, now we restart on old settings, but for inlay hints (and document symbols in #66690) we don't need to.

);
if (symbol) {
const declarations = symbol.getDeclarations();
if (declarations && declarations.length > 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getGlobalDomType currently performs full symbol lookups for every generic DOM event. A small cache would avoid redundant lookups.

Copy link
Copy Markdown
Contributor Author

@kbrilla kbrilla Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now uses WeakMap<ts.TypeChecker, Map<string, ts.Type | null>> to reuse it across sequentail request (for example typing)

Comment on lines +1487 to +1521
function unwrapAngularType(type: ts.Type, typeChecker: ts.TypeChecker): ts.Type {
const typeRef = type as ts.TypeReference;
const typeArgs = typeRef.typeArguments;

if (!typeArgs || typeArgs.length === 0) {
return type;
}

// Get the type name to check what kind of wrapper this is
const typeName = typeChecker.typeToString(type);

// Output wrappers: EventEmitter<T>, OutputEmitterRef<T>, Observable<T>, Subject<T>
// The first type argument is the event/value type
if (
typeName.startsWith('EventEmitter<') ||
typeName.startsWith('OutputEmitterRef<') ||
typeName.startsWith('Observable<') ||
typeName.startsWith('Subject<')
) {
return typeArgs[0];
}

// Input signal wrappers: InputSignal<T>, ModelSignal<T>
// The first type argument is the value type
if (typeName.startsWith('InputSignal<') || typeName.startsWith('ModelSignal<')) {
return typeArgs[0];
}

// InputSignalWithTransform<T, TransformT> - first arg is the output type
if (typeName.startsWith('InputSignalWithTransform<')) {
return typeArgs[0];
}

return type;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function unwrapAngularType(type: ts.Type, typeChecker: ts.TypeChecker): ts.Type {
const typeRef = type as ts.TypeReference;
const typeArgs = typeRef.typeArguments;
if (!typeArgs || typeArgs.length === 0) {
return type;
}
// Get the type name to check what kind of wrapper this is
const typeName = typeChecker.typeToString(type);
// Output wrappers: EventEmitter<T>, OutputEmitterRef<T>, Observable<T>, Subject<T>
// The first type argument is the event/value type
if (
typeName.startsWith('EventEmitter<') ||
typeName.startsWith('OutputEmitterRef<') ||
typeName.startsWith('Observable<') ||
typeName.startsWith('Subject<')
) {
return typeArgs[0];
}
// Input signal wrappers: InputSignal<T>, ModelSignal<T>
// The first type argument is the value type
if (typeName.startsWith('InputSignal<') || typeName.startsWith('ModelSignal<')) {
return typeArgs[0];
}
// InputSignalWithTransform<T, TransformT> - first arg is the output type
if (typeName.startsWith('InputSignalWithTransform<')) {
return typeArgs[0];
}
return type;
}
function unwrapAngularType(type: ts.Type, typeChecker: ts.TypeChecker): ts.Type {
const typeRef = type as ts.TypeReference;
const typeArgs = typeRef.typeArguments ?? typeRef.aliasTypeArguments;
if (!typeArgs || typeArgs.length === 0) {
return type;
}
const symbolName = type.symbol?.name ?? type.aliasSymbol?.name;
if (
symbolName === 'EventEmitter' ||
symbolName === 'OutputEmitterRef' ||
symbolName === 'Observable' ||
symbolName === 'Subject' ||
symbolName === 'InputSignal' ||
symbolName === 'ModelSignal' ||
symbolName === 'InputSignalWithTransform'
) {
return typeArgs[0];
}
return type;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applied

return undefined;
}

const sourceFile = ts.createSourceFile(part.file, text, ts.ScriptTarget.Latest, true);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using ts.createSourceFile to convert offsets to line/character numbers for potentially hundreds of inlay hints... This O(N) operation (where N is file size) will degrade performance significantly. session.projectService.getScriptInfo(part.file).positionToLineOffset(offset) utilizes the cached line map (O(log N)).

 const scriptInfo = session.projectService.getScriptInfo(part.file);
  if (scriptInfo) {
    const start = scriptInfo.positionToLineOffset(part.span.start);
    const end = scriptInfo.positionToLineOffset(part.span.start + part.span.length);
    // Note: lsp.Location expects 0-indexed positions, whereas positionToLineOffset returns 1-indexed
    return {
      uri: filePathToUri(part.file),
      range: {
        start: {line: start.line - 1, character: start.offset - 1},
        end: {line: end.line - 1, character: end.offset - 1},
      },
    };
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, used this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, @atscott, thank you for your time you spent on those PR, appreciate it!

…ettings grouping

Add shared workspace configuration helper utilities and move extension settings to grouped configuration sections, without inlay-hint feature options yet.
@kbrilla kbrilla force-pushed the feat/inlay-hints branch 2 times, most recently from 1af5341 to b20a659 Compare March 4, 2026 00:57
@kbrilla kbrilla requested a review from atscott March 4, 2026 01:05
Copy link
Copy Markdown
Contributor

@atscott atscott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for contributing this cool feature!

@atscott atscott added action: merge The PR is ready for merge by the caretaker target: minor This PR is targeted for the next minor release and removed action: merge The PR is ready for merge by the caretaker labels Mar 4, 2026
@atscott
Copy link
Copy Markdown
Contributor

atscott commented Mar 4, 2026

@kbrilla could you fix the lint error? Otherwise this looks good to go

@kbrilla kbrilla force-pushed the feat/inlay-hints branch from b20a659 to ad713b2 Compare March 5, 2026 09:18
@angular-robot angular-robot Bot requested a review from atscott March 5, 2026 09:18
@kbrilla kbrilla force-pushed the feat/inlay-hints branch from ad713b2 to 8ec9ed1 Compare March 5, 2026 09:44
@atscott atscott force-pushed the feat/inlay-hints branch from 8ec9ed1 to 394ce7f Compare March 5, 2026 19:55
@atscott atscott added the action: merge The PR is ready for merge by the caretaker label Mar 5, 2026
kbrilla added 3 commits March 5, 2026 22:21
This refactoring improves the lifecycle management and configuration handling within the VS Code extension's language client.

Key changes include:
- Introduced sessionDisposables to manage resources tied to the lifespan of an LSP client session, ensuring they are properly cleaned up when the client stops without affecting global extension resources.
- Added a configuration change listener to clear the fileToIsInAngularProjectMap cache, ensuring project state is re-evaluated when settings change.
- Enhanced registerNotificationHandlers to clear the project state cache when project loading begins or completes, providing more accurate "is in Angular project" checks.
- Modified isInAngularProject to only cache positive results, allowing for recovery if a file was initially incorrectly identified as being outside an Angular project.
Add Angular template inlay hints end-to-end across language-service and VS Code extension server/client wiring, including inlay-specific configuration mapping, request guards, and refresh behavior.
…erage

test(language-service): add inlay hint regression and integration coverage
@atscott atscott force-pushed the feat/inlay-hints branch from 394ce7f to f9a3162 Compare March 5, 2026 22:23
@atscott atscott merged commit cc64def into angular:main Mar 6, 2026
17 of 19 checks passed
@atscott
Copy link
Copy Markdown
Contributor

atscott commented Mar 6, 2026

This PR was merged into the repository. The changes were merged into the following branches:

@angular-automatic-lock-bot
Copy link
Copy Markdown

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot Bot locked and limited conversation to collaborators Apr 6, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

action: merge The PR is ready for merge by the caretaker area: docs Related to the documentation area: language-service Issues related to Angular's VS Code language service area: vscode-extension Issues related to the Angular Language Service VsCode extension detected: feature PR contains a feature commit target: minor This PR is targeted for the next minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants