feat: Add Angular, HTML, and SCSS / Sass language servers (#1439)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Pavlo Basanets <Pavlo_Basanets@epam.com>
This commit is contained in:
authored and GitHub committed 2026-05-05 14:33:51 +02:00
1 parent e643e91cd7
commit 3122384822
53 files changed
+3280 -22

No files matched your search

@@ -0,0 +1,5 @@
node_modules
.angular
dist
out-tsc
.angular-install.lock
@@ -0,0 +1,24 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"serena-test-app": {
"projectType": "application",
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/serena-test-app",
"index": "src/index.html",
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json"
}
}
}
}
}
}
+134
View File
@@ -0,0 +1,134 @@
{
"name": "serena-angular-test-repo",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "serena-angular-test-repo",
"version": "0.0.0",
"dependencies": {
"@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/platform-browser": "^21.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.6.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"typescript": "~5.9.0"
}
},
"node_modules/@angular/common": {
"version": "21.2.10",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.10.tgz",
"integrity": "sha512-WLyi/CRLtgALg2mmaqIuKuPnE4i+8PGt/uuz26pVqx+ASh28/TWr5KSCAMomgxEc8kt4OE7lopoQsTihrQCfEw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/core": "21.2.10",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/compiler": {
"version": "21.2.10",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.10.tgz",
"integrity": "sha512-IrgdFuzzD7NTK3WQaSfowjAPxPbnTqsgR92NsOs5ZaWu3RgLl21dHThNc0BK1KwVwppLUSWmD4qePbcLW71VzQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/@angular/core": {
"version": "21.2.10",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.10.tgz",
"integrity": "sha512-uxH+mbPiCE7rInWKYOPe9Ytas97+mFM6FhFORoN234yBK3b8he+iDuxX6dsbhEFCxhRmfS6hLxe7BdLY6U6kIA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/compiler": "21.2.10",
"rxjs": "^6.5.3 || ^7.4.0",
"zone.js": "~0.15.0 || ~0.16.0"
},
"peerDependenciesMeta": {
"@angular/compiler": {
"optional": true
},
"zone.js": {
"optional": true
}
}
},
"node_modules/@angular/platform-browser": {
"version": "21.2.10",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.10.tgz",
"integrity": "sha512-5WMoHGU8BOV3eO9h3vGMIUDPf+3SHis7+X2dHKMtKfFBUtiO8m/lq2x3PzkkKj1782i7KYt92EqPHuADd/eWOw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/animations": "21.2.10",
"@angular/common": "21.2.10",
"@angular/core": "21.2.10"
},
"peerDependenciesMeta": {
"@angular/animations": {
"optional": true
}
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/zone.js": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
"license": "MIT"
}
}
}
@@ -0,0 +1,18 @@
{
"name": "serena-angular-test-repo",
"version": "0.0.0",
"private": true,
"description": "Minimal Angular workspace fixture used by Serena language-server tests.",
"dependencies": {
"@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/platform-browser": "^21.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.6.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"typescript": "~5.9.0"
}
}
@@ -0,0 +1,21 @@
<section>
<h1>{{ title() | exclaim }}</h1>
<p>{{ greeting() }}</p>
<input
type="text"
[value]="userName()"
(input)="setName($any($event.target).value)"
placeholder="Enter your name"
/>
@if (items().length > 0) {
<ul>
@for (item of items(); track item) {
<app-item-card [label]="item"></app-item-card>
}
</ul>
} @else {
<p>No items.</p>
}
</section>
@@ -0,0 +1,26 @@
import { Component, signal } from '@angular/core';
import { GreetingService } from './greeting.service';
import { ItemCardComponent } from './item-card.component';
import { ExclaimPipe } from './exclaim.pipe';
@Component({
selector: 'app-root',
standalone: true,
imports: [ItemCardComponent, ExclaimPipe],
templateUrl: './app.component.html',
})
export class AppComponent {
readonly title = signal('Serena Angular Test');
readonly userName = signal('');
readonly items = signal<string[]>(['alpha', 'beta', 'gamma']);
constructor(private readonly greetings: GreetingService) {}
greeting(): string {
return this.greetings.greet(this.userName() || undefined);
}
setName(name: string): void {
this.userName.set(name);
}
}
@@ -0,0 +1,6 @@
<!-- Template path: ngserver flags the reference to ``undefinedSignal()`` since
it has no matching field on DiagnosticsSampleComponent. The standalone .html
diagnostic exercises the Angular template compiler, not plain-HTML validation. -->
<section>
<h1>{{ undefinedSignal() }}</h1>
</section>
@@ -0,0 +1,15 @@
import { Component } from '@angular/core';
// Two diagnostic sources in one fixture:
// * the type error on ``count`` is reported by tsserver via the Angular plugin
// when diagnosing this .ts file directly.
// * the ``undefinedSignal()`` reference in diagnostics_sample.html is reported
// by ngserver when diagnosing the template attached via ``templateUrl``.
@Component({
selector: 'app-diagnostics-sample',
standalone: true,
templateUrl: './diagnostics_sample.html',
})
export class DiagnosticsSampleComponent {
readonly count: number = 'not-a-number';
}
@@ -0,0 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'exclaim',
standalone: true,
})
export class ExclaimPipe implements PipeTransform {
transform(value: string, count: number = 1): string {
return value + '!'.repeat(count);
}
}
@@ -0,0 +1,3 @@
export interface Greeter {
greet(name?: string): string;
}
@@ -0,0 +1,11 @@
import { Injectable } from '@angular/core';
import { Greeter } from './greeter.interface';
@Injectable({ providedIn: 'root' })
export class GreetingService implements Greeter {
private readonly defaultName = 'World';
greet(name?: string): string {
return `Hello, ${name ?? this.defaultName}!`;
}
}
@@ -0,0 +1,18 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-item-card',
standalone: true,
template: `
<li class="item-card" [attr.data-label]="label">
<strong>{{ label }}</strong>
</li>
`,
})
export class ItemCardComponent {
@Input() label: string = '';
formatLabel(): string {
return this.label.trim();
}
}
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Serena Angular Test</title>
</head>
<body>
<app-root></app-root>
</body>
</html>
@@ -0,0 +1,4 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent).catch((err) => console.error(err));
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.html"]
}
@@ -0,0 +1,25 @@
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>About — Serena HTML Test</title>
<link rel="stylesheet" href="styles/main.css">
</head>
<body>
<header id="page-header">
<h1 id="about-title">About</h1>
<nav id="main-nav">
<a href="index.html">Home</a>
</nav>
</header>
<main id="page-main">
<article id="about-article">
<h2>About this repo</h2>
<p>Used by the Serena language server tests.</p>
</article>
</main>
</body>
</html>
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<!-- Diagnostics smoke fixture — vscode-html-language-server does not emit
diagnostics for HTML structure; this file just gives the LS something to
chew on so the diagnostics endpoint round-trip can be exercised. -->
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Diagnostics Sample</title>
</head>
<body>
<div id="dup">first</div>
<div id="dup">second</div>
</body>
</html>
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Serena HTML Test</title>
<link rel="stylesheet" href="styles/main.css">
</head>
<body>
<header id="page-header">
<h1 id="site-title">Serena HTML Test Repo</h1>
<nav id="main-nav">
<a href="about.html">About</a>
<a href="#section-features">Features</a>
</nav>
</header>
<main id="page-main">
<section id="section-intro">
<h2>Introduction</h2>
<p>A minimal HTML test fixture used by the Serena language-server tests.</p>
</section>
<section id="section-features">
<h2>Features</h2>
<ul id="feature-list">
<li>Symbol detection</li>
<li>Element navigation</li>
<li>Cross-file links</li>
</ul>
</section>
</main>
<footer id="page-footer">
<p>&copy; Serena</p>
</footer>
<script src="scripts/app.js"></script>
</body>
</html>
@@ -0,0 +1,17 @@
@use "variables" as vars;
// Common card-style container used by buttons, panels, etc.
@mixin card-surface($bg: vars.$color-bg) {
background-color: $bg;
border-radius: 4px;
padding: vars.$space-md;
}
@mixin focus-ring($color: vars.$color-primary) {
outline: 2px solid $color;
outline-offset: 2px;
}
@function rem($px) {
@return ($px / 16) * 1rem;
}
@@ -0,0 +1,10 @@
// Color tokens used across the design system.
$color-primary: #2563eb;
$color-secondary: #f97316;
$color-text: #1a1a1a;
$color-bg: #ffffff;
// Spacing scale.
$space-sm: 0.5rem;
$space-md: 1rem;
$space-lg: 2rem;
@@ -0,0 +1,22 @@
@use "variables" as vars;
@use "mixins" as mix;
.button {
@include mix.card-surface();
color: vars.$color-text;
cursor: pointer;
&:focus-visible {
@include mix.focus-ring();
}
}
.button-primary {
@include mix.card-surface(vars.$color-primary);
color: white;
}
.button-secondary {
@include mix.card-surface(vars.$color-secondary);
color: white;
}
@@ -0,0 +1,13 @@
/*
* Plain-CSS diagnostics sample. The vscode-css-languageservice engine inside
* some-sass reports parser errors here even with lint diagnostics off — the
* unterminated rule body and stray brace produce syntax-level diagnostics.
*/
.broken {
color: red;
background:
}
.also-broken } {
margin: 0;
}
@@ -0,0 +1,41 @@
@import "reset.css";
@import "theme.css";
body {
font-family: var(--font-body);
color: var(--color-text);
background-color: var(--color-bg);
margin: 0;
}
#page-header {
padding: 1rem;
background-color: var(--color-primary);
}
#site-title {
margin: 0;
font-size: 1.5rem;
}
.button {
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
.button-primary {
background-color: var(--color-primary);
color: white;
}
.button-secondary {
background-color: transparent;
border: 1px solid var(--color-primary);
}
@media (max-width: 600px) {
#site-title {
font-size: 1.2rem;
}
}
@@ -0,0 +1,18 @@
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
}
a {
color: inherit;
text-decoration: none;
}
ul {
list-style: none;
padding: 0;
}
@@ -0,0 +1,15 @@
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-primary: #2563eb;
--color-secondary: #f97316;
--font-body: "Inter", system-ui, sans-serif;
--font-heading: "Inter", system-ui, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0f172a;
--color-text: #f1f5f9;
}
}
@@ -0,0 +1,5 @@
// Some Sass flags references to undefined variables and missing semicolons.
.broken {
color: $undefined-variable
background: $also-not-defined;
}
@@ -0,0 +1,16 @@
@use "variables" as vars;
@use "mixins" as mix;
@forward "buttons";
body {
background-color: vars.$color-bg;
color: vars.$color-text;
margin: 0;
padding: vars.$space-lg;
font-size: mix.rem(16);
}
.panel {
@include mix.card-surface();
margin-bottom: vars.$space-md;
}