feat(language-service): add Angular-specific Inlay Hints (LSP 3.17)#66755
feat(language-service): add Angular-specific Inlay Hints (LSP 3.17)#66755atscott merged 4 commits intoangular:mainfrom
Conversation
| const tsHints = languageService.provideInlayHints(scriptInfo.fileName, span, tsPreferences); | ||
|
|
||
| for (const tsHint of tsHints) { | ||
| const position = scriptInfo.positionToLineOffset(tsHint.position); | ||
| hints.push(convertTsInlayHint(tsHint, position)); | ||
| } |
There was a problem hiding this comment.
From my testing, this should be omitted. Typescript provides its own inlay hints and we should not duplicate them.
There was a problem hiding this comment.
I removed TS inlay hints from Angular response
| "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": { |
There was a problem hiding this comment.
I think all of these should be defaulted to their "off" equivalents
There was a problem hiding this comment.
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.
| "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": { |
|
@kbrilla Do you plan on continuing work on this PR? |
|
, I was not able to look into it today, but will continue tomorrow |
1197eaa to
d5f5c93
Compare
c1e6168 to
abefeba
Compare
|
Updated PR description and added screenshots |
a088340 to
4ccf175
Compare
| { | ||
| "name": "ng-template", | ||
| "displayName": "Angular Language Service", | ||
| "displayName": "Angular", |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
was this removal intentional?
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
I think this change should move to the next commit
| ); | ||
|
|
||
| this.disposables.push( | ||
| vscode.workspace.onDidChangeConfiguration(() => { |
There was a problem hiding this comment.
Why are we removing the stop/start of the language client? Don't some settings invalidate the session configuration?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
getGlobalDomType currently performs full symbol lookups for every generic DOM event. A small cache would avoid redundant lookups.
There was a problem hiding this comment.
now uses WeakMap<ts.TypeChecker, Map<string, ts.Type | null>> to reuse it across sequentail request (for example typing)
| 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; | ||
| } |
There was a problem hiding this comment.
| 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; | |
| } |
| return undefined; | ||
| } | ||
|
|
||
| const sourceFile = ts.createSourceFile(part.file, text, ts.ScriptTarget.Latest, true); |
There was a problem hiding this comment.
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},
},
};
}
There was a problem hiding this comment.
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.
1af5341 to
b20a659
Compare
atscott
left a comment
There was a problem hiding this comment.
Thanks for contributing this cool feature!
|
@kbrilla could you fix the lint error? Otherwise this looks good to go |
b20a659 to
ad713b2
Compare
ad713b2 to
8ec9ed1
Compare
8ec9ed1 to
394ce7f
Compare
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
394ce7f to
f9a3162
Compare
|
This PR was merged into the repository. The changes were merged into the following branches:
|
|
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |


PR Checklist
PR Type
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:
textDocument/inlayHintandinlayHint/resolvesupport in the extension server.Does this PR introduce a breaking change?
Other information
Screenshots