mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
build: migrate vscode extension into repo (#63924)
Migrate the vscode extension for angular into this repository. PR Close #63924
This commit is contained in:
committed by
Jessica Janiuk
parent
25e93f0d01
commit
863c7eaafe
+7
-1
@@ -4,7 +4,7 @@
|
||||
# https://github.com/bazelbuild/bazel/issues/8106
|
||||
.git
|
||||
node_modules
|
||||
dist
|
||||
dist/
|
||||
|
||||
# All integration test node_modules folders
|
||||
integration/animations/node_modules
|
||||
@@ -39,6 +39,7 @@ integration/legacy-animations/node_modules
|
||||
integration/legacy-animations-async/node_modules
|
||||
integration/typings_test_ts59/node_modules
|
||||
modules/ssr-benchmarks/node_modules
|
||||
vscode-ng-language-service/integration/project/dist/
|
||||
|
||||
# For rules_js
|
||||
adev/node_modules
|
||||
@@ -69,3 +70,8 @@ packages/service-worker/node_modules
|
||||
packages/zone.js/test/typings/node_modules
|
||||
packages/zone.js/node_modules
|
||||
tools/bazel/rules_angular_store/node_modules
|
||||
vscode-ng-language-service/node_modules
|
||||
vscode-ng-language-service/integration/pre_standalone_project/node_modules
|
||||
vscode-ng-language-service/integration/workspace/node_modules
|
||||
vscode-ng-language-service/integration/pre_apf_project/node_modules
|
||||
vscode-ng-language-service/integration/project/node_modules
|
||||
|
||||
@@ -119,6 +119,21 @@ jobs:
|
||||
- name: Build adev in fast mode to ensure it continues to work
|
||||
run: pnpm bazel build //adev:build
|
||||
|
||||
vscode-ng-language-service:
|
||||
runs-on:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@d776aff2258f15fc3dc1e63852887518eec3ad75
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@d776aff2258f15fc3dc1e63852887518eec3ad75
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@d776aff2258f15fc3dc1e63852887518eec3ad75
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run tests
|
||||
run: pnpm bazel test //vscode-ng-language-service/...
|
||||
|
||||
publish-snapshots:
|
||||
runs-on:
|
||||
labels: ubuntu-latest
|
||||
|
||||
@@ -127,6 +127,21 @@ jobs:
|
||||
- name: Build adev in fast mode to ensure it continues to work
|
||||
run: pnpm bazel build //adev:build
|
||||
|
||||
vscode-ng-language-service:
|
||||
runs-on:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@d776aff2258f15fc3dc1e63852887518eec3ad75
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@d776aff2258f15fc3dc1e63852887518eec3ad75
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@d776aff2258f15fc3dc1e63852887518eec3ad75
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run tests
|
||||
run: pnpm bazel test //vscode-ng-language-service/...
|
||||
|
||||
zone-js:
|
||||
runs-on:
|
||||
labels: ubuntu-latest
|
||||
|
||||
@@ -16,9 +16,6 @@ pubspec.lock
|
||||
!.devcontainer/recommended-devcontainer.json
|
||||
!.devcontainer/recommended-Dockerfile
|
||||
.settings/
|
||||
.vscode/launch.json
|
||||
.vscode/settings.json
|
||||
.vscode/tasks.json
|
||||
*.swo
|
||||
*.swp
|
||||
modules/.settings
|
||||
|
||||
@@ -25,6 +25,7 @@ export const commitMessage: CommitMessageConfig = {
|
||||
'forms',
|
||||
'http',
|
||||
'language-service',
|
||||
'language-server',
|
||||
'localize',
|
||||
'migrations',
|
||||
'packaging',
|
||||
@@ -34,6 +35,7 @@ export const commitMessage: CommitMessageConfig = {
|
||||
'router',
|
||||
'service-worker',
|
||||
'upgrade',
|
||||
'vscode-extension',
|
||||
'zone.js',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -25,6 +25,9 @@ export const format: FormatConfig = {
|
||||
|
||||
// Ignore generated javascript file(s)
|
||||
'!.github/actions/deploy-docs-site/main.js',
|
||||
|
||||
// Ignore testing data files for language service
|
||||
'!vscode-ng-language-service/syntaxes/test/data/*.ts',
|
||||
],
|
||||
},
|
||||
'buildifier': true,
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ function readPackage(pkg, context) {
|
||||
// Change all locally generated packages to directly depend on the other local packages, instead
|
||||
// of expecting them as peerDependencies automatically as we do not auto install peer deps. The
|
||||
// package is also removed from peerDependencies as it was moved over and will just cause errors.
|
||||
if (localAngularPackages.includes(key)) {
|
||||
if (localAngularPackages.includes(key) && pkg.version === '0.0.0-PLACEHOLDER') {
|
||||
pkg.dependencies = {
|
||||
...pkg.dependencies,
|
||||
[key]: 'workspace: *',
|
||||
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
// A launch configuration that compiles the extension and then opens it inside a new window
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"name": "Launch Dev Client",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--disable-extensions",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/bin/vscode-ng-language-service/client/src/*.js"
|
||||
],
|
||||
"preLaunchTask": {
|
||||
"type": "npm",
|
||||
"script": "vscode:watch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"name": "Launch Prod Client",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/dist/bin/vscode-ng-language-service/npm"
|
||||
],
|
||||
"sourceMaps": false,
|
||||
"preLaunchTask": "build vsix"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to Server",
|
||||
"port": 6009,
|
||||
"restart": true,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/bin/vscode-ng-language-service/server/src/*.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to Jasmine",
|
||||
"port": 9229,
|
||||
"restart": true,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/bin/vscode-ng-language-service/**/*.js"
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Integration test: Attach to server",
|
||||
"port": 9330,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/bin/vscode-ng-language-service/integration/lsp/*.js"
|
||||
],
|
||||
"type": "node"
|
||||
},
|
||||
{
|
||||
"name": "E2E Test",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--disable-extensions",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/dist/bin/vscode-ng-language-service/npm",
|
||||
"--extensionTestsPath=${workspaceFolder}/dist/bin/vscode-ng-language-service/integration/e2e/jasmine",
|
||||
"${workspaceFolder}/vscode-ng-language-service/integration/project"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/bin/vscode-ng-language-service/integration/e2e/*.js"
|
||||
],
|
||||
"preLaunchTask": {
|
||||
"type": "npm",
|
||||
"script": "compile:integration"
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Dev Client + Server",
|
||||
"configurations": [
|
||||
"Launch Dev Client",
|
||||
"Attach to Server"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "compile",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"panel": "dedicated",
|
||||
"reveal": "never"
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$tsc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "vscode:watch",
|
||||
"isBackground": true,
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"panel": "dedicated",
|
||||
"reveal": "never"
|
||||
},
|
||||
"problemMatcher": {
|
||||
"base": "$tsc-watch",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "^iBazel \\[\\d{1,2}:\\d{1,2}(?:AM|PM)\\]: Querying for files to watch.*",
|
||||
"endsPattern": "^INFO: Build completed successfully, \\d+ total action(s)?"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "build vsix",
|
||||
"command": "pnpm bazel build //vscode-ng-language-service:npm --config=release"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -96,6 +96,10 @@ npm.npm_translate_lock(
|
||||
"//packages/service-worker:package.json",
|
||||
"//packages/upgrade:package.json",
|
||||
"//tools/bazel/rules_angular_store:package.json",
|
||||
"//vscode-ng-language-service/integration/pre_apf_project:package.json",
|
||||
"//vscode-ng-language-service/integration/pre_standalone_project:package.json",
|
||||
"//vscode-ng-language-service/integration/project:package.json",
|
||||
"//vscode-ng-language-service/integration/workspace:package.json",
|
||||
],
|
||||
npmrc = "//:.npmrc",
|
||||
pnpm_lock = "//:pnpm-lock.yaml",
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
"ng-dev": "tsx --tsconfig .ng-dev/tsconfig.json node_modules/@angular/ng-dev/bundles/cli.mjs",
|
||||
"build": "tsx --tsconfig scripts/tsconfig.json scripts/build/build-packages-dist.mts",
|
||||
"test": "bazelisk test",
|
||||
"test:ci": "bazelisk test -- //... -//integration/... -//adev/... -//devtools/... -//modules/ssr-benchmarks/...",
|
||||
"test:ci": "bazelisk test -- //... -//integration/... -//adev/... -//vscode-ng-language-service/... -//devtools/... -//modules/ssr-benchmarks/...",
|
||||
"integration-tests:ci": "bazelisk test -- //integration/...",
|
||||
"test-tsec": "bazelisk test //... --build_tag_filters=tsec --test_tag_filters=tsec",
|
||||
"lint": "pnpm --silent tslint && pnpm --silent ng-dev format changed --check",
|
||||
|
||||
Generated
+1757
-66
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,11 @@ packages:
|
||||
- packages/zone.js/
|
||||
- packages/zone.js/test/typings/
|
||||
- tools/bazel/rules_angular_store/
|
||||
- vscode-ng-language-service
|
||||
- vscode-ng-language-service/integration/pre_standalone_project
|
||||
- vscode-ng-language-service/integration/workspace
|
||||
- vscode-ng-language-service/integration/pre_apf_project
|
||||
- vscode-ng-language-service/integration/project
|
||||
|
||||
allowedDeprecatedVersions:
|
||||
angular: '*'
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
load("@aspect_bazel_lib//lib:expand_template.bzl", "expand_template_rule")
|
||||
load("@aspect_rules_js//js:defs.bzl", "js_library")
|
||||
load("@aspect_rules_js//npm:defs.bzl", "npm_package")
|
||||
load("@aspect_rules_ts//ts:defs.bzl", "ts_config")
|
||||
load("@npm//:defs.bzl", "npm_link_all_packages")
|
||||
load("@npm//vscode-ng-language-service:vsce/package_json.bzl", vsce_bin = "bin")
|
||||
|
||||
npm_link_all_packages(name = "node_modules")
|
||||
|
||||
js_library(
|
||||
name = "package_json",
|
||||
srcs = ["package.json"],
|
||||
visibility = ["//vscode-ng-language-service/syntaxes/test:__pkg__"],
|
||||
)
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
expand_template_rule(
|
||||
name = "package_json_expanded",
|
||||
out = "package_expanded.json",
|
||||
stamp_substitutions = {
|
||||
"0.0.0-PLACEHOLDER": "{{BUILD_SCM_VERSION}}",
|
||||
},
|
||||
substitutions = {
|
||||
"./bazel-bin/client/src/extension": "./index",
|
||||
},
|
||||
template = "package.json",
|
||||
)
|
||||
|
||||
npm_package(
|
||||
name = "vsix_sandbox",
|
||||
srcs = [
|
||||
"package_expanded.json",
|
||||
"angular.png",
|
||||
"CHANGELOG.md",
|
||||
"README.md",
|
||||
"//vscode-ng-language-service/client:index.js",
|
||||
"//vscode-ng-language-service/server:npm_files",
|
||||
"//vscode-ng-language-service/syntaxes:npm_files",
|
||||
# Transitive closure of npm deps needed for vsce; this set was determined manually by
|
||||
# running `bazel build //:vsix` and burning down missing packages. We do this so that we
|
||||
# don't have to run an additional `npm install` action to create a node_modules within the
|
||||
# //:npm npm_package where the vsce build takes place.
|
||||
"//vscode-ng-language-service:node_modules/@angular/language-service",
|
||||
"//vscode-ng-language-service:node_modules/balanced-match",
|
||||
"//vscode-ng-language-service:node_modules/brace-expansion",
|
||||
"//vscode-ng-language-service:node_modules/concat-map",
|
||||
"//vscode-ng-language-service:node_modules/lru-cache",
|
||||
"//vscode-ng-language-service:node_modules/minimatch",
|
||||
"//vscode-ng-language-service:node_modules/semver",
|
||||
"//vscode-ng-language-service:node_modules/typescript",
|
||||
"//vscode-ng-language-service:node_modules/vscode-jsonrpc",
|
||||
"//vscode-ng-language-service:node_modules/vscode-html-languageservice",
|
||||
"//vscode-ng-language-service:node_modules/vscode-languageclient",
|
||||
"//vscode-ng-language-service:node_modules/vscode-languageserver-protocol",
|
||||
"//vscode-ng-language-service:node_modules/vscode-languageserver-textdocument",
|
||||
"//vscode-ng-language-service:node_modules/vscode-languageserver-types",
|
||||
"//vscode-ng-language-service:node_modules/vscode-languageserver",
|
||||
"//vscode-ng-language-service:node_modules/vscode-nls",
|
||||
"//vscode-ng-language-service:node_modules/vscode-uri",
|
||||
"//vscode-ng-language-service:node_modules/yallist",
|
||||
],
|
||||
exclude_srcs_patterns = [
|
||||
"*.map",
|
||||
"**/*.map",
|
||||
],
|
||||
include_srcs_packages = [
|
||||
"**",
|
||||
],
|
||||
replace_prefixes = {
|
||||
"package_expanded.json": "package.json",
|
||||
"server/package_expanded.json": "server/package.json",
|
||||
"client/": "",
|
||||
"syntaxes/src/": "syntaxes/",
|
||||
},
|
||||
)
|
||||
|
||||
vsce_bin.vsce(
|
||||
name = "vsix",
|
||||
srcs = [
|
||||
":vsix_sandbox",
|
||||
],
|
||||
outs = ["ng-template.vsix"],
|
||||
args = [
|
||||
"package",
|
||||
"-o",
|
||||
"../ng-template.vsix",
|
||||
],
|
||||
chdir = "vscode-ng-language-service/vsix_sandbox",
|
||||
# vsce requires npm on the PATH; we can get this from the Bazel rules_nodejs but it is not
|
||||
# included by default in rules_js binary rules so we include it here explicitly
|
||||
include_npm = True,
|
||||
)
|
||||
|
||||
npm_package(
|
||||
name = "npm",
|
||||
srcs = [
|
||||
":vsix",
|
||||
":vsix_sandbox",
|
||||
],
|
||||
root_paths = [
|
||||
"vsix_sandbox",
|
||||
],
|
||||
visibility = ["//vscode-ng-language-service/integration:__subpackages__"],
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
# Monorepo setup
|
||||
|
||||
This repository is a monorepo. All build outputs are created in the `bazel-bin` directory.
|
||||
|
||||
The `dependencies` in root [`package.json`](package.json) must include all production dependencies
|
||||
of the client **and** and the server.
|
||||
|
||||
The dependencies of the `server` need to be duplicated in the root [`package.json`](package.json).
|
||||
Therefore, if a new prod dependency is added to the server, it needs to be
|
||||
added to both [`package.json`](package.json) and [`server/package.json`](server/package.json).
|
||||
|
||||
## Navigating Projects in IDE
|
||||
|
||||
This repository contains multiple TypeScript projects, each with its own `tsconfig.json`.
|
||||
TypeScript's [project references](https://www.typescriptlang.org/docs/handbook/project-references.html) are used to handle the builds.
|
||||
As a result, some projects depend on the `.d.ts` output of their dependencies, like `client`
|
||||
and `server` depend on `common`. In order to navigate the projects in your IDE,
|
||||
you need to first build them by running
|
||||
```shell
|
||||
pnpm compile
|
||||
```
|
||||
|
||||
If you'd like to force a clean build, you can delete the existing build artifacts
|
||||
by running
|
||||
```shell
|
||||
pnpm bazel build //:npm --config=release
|
||||
```
|
||||
|
||||
## Formatting source code
|
||||
|
||||
This repository uses a common toolset provided in the angular organization for formatting, `ng-dev`. It is installed as a standard npm package,
|
||||
and can be run locally with `pnpm format`.
|
||||
|
||||
## Test Local Changes in VSCode
|
||||
|
||||
Any changes made to the code in this repository or the upstream
|
||||
`@angular/language-service` package can be immediately tested out in a
|
||||
development version of VSCode. The instructions below explain how to bring up
|
||||
a local instance and then install a local version of `@angular/language-service`.
|
||||
|
||||
### Check TypeScript version
|
||||
|
||||
First, make sure that the TypeScript version in `@angular/angular` is the same
|
||||
as that used in this repository. If not, update the following files:
|
||||
|
||||
1. `typescript` dependency in [`package.json`](package.json)
|
||||
2. `MIN_TS_VERSION` in [`version_provider.ts`](server/src/version_provider.ts)
|
||||
|
||||
### Launch VSCode in Extension Development Host
|
||||
|
||||
The scripts in `.vscode` directory are setup to automatically compile the code,
|
||||
then launch a new instance of VSCode with the Angular extension installed.
|
||||
To do so, either
|
||||
|
||||
1. Press F5, or
|
||||
2. Go to Run on the sidebar, select `Launch Client` from the task list
|
||||
|
||||
After the client is launched, you can optionally choose to attach a debugger to
|
||||
the local instance. To do so,
|
||||
|
||||
1. Go to Run on the sidebar, select `Attach to Server` from the task list
|
||||
|
||||
As a shortcut, there's also a task setup to automatically launch the client and
|
||||
attach the debugger in a single step. To do so,
|
||||
|
||||
1. Go to Run on the sidebar, select `Client + Server` from the task list
|
||||
|
||||
Note: Because we are building with bazel, any breakpoints need to be set on the files in `bazel-bin/...` or by adding a `debugger;` statement before compilation.
|
||||
|
||||
### Install Local `@angular/language-service`
|
||||
|
||||
If changes are made to the upstream language service package, they can also be
|
||||
tested locally. This involves building the NPM package, then updating the server
|
||||
dependency. To do so, run the following script from the `@angular/angular`
|
||||
repository.
|
||||
|
||||
```bash
|
||||
./packages/language-service/build.sh /path/to/vscode-ng-language-service
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
# Angular Language Service
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
This extension provides a rich editing experience for Angular templates, both inline
|
||||
and external templates including:
|
||||
|
||||
* Completions lists
|
||||
* AOT Diagnostic messages
|
||||
* Quick info
|
||||
* Go to definition
|
||||
|
||||
## Download
|
||||
|
||||
Download the extension from [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=Angular.ng-template).
|
||||
|
||||
## Configuring compiler options for the Angular Language Service
|
||||
|
||||
The Angular Language Service uses the same set of options that are used to compile the application.
|
||||
To get the most complete information in the editor, set the `strictTemplates` option in `tsconfig.json`,
|
||||
as shown in the following example:
|
||||
|
||||
```
|
||||
"angularCompilerOptions": {
|
||||
"strictTemplates": true
|
||||
}
|
||||
```
|
||||
|
||||
For more information, see the [Angular compiler options](https://angular.io/guide/angular-compiler-options) guide.
|
||||
|
||||
## Versioning
|
||||
|
||||
The language service extension relies on the `@angular/language-service` and `typescript` packages
|
||||
for its backend. `@angular/language-service` is always bundled with the extension, and is always
|
||||
the latest version at the time of the release.
|
||||
`typescript` is loaded, in order of priority, from:
|
||||
|
||||
1. The path specified by `typescript.tsdk` in project or global settings.
|
||||
2. _(Recommended)_ The version of `typescript` bundled with the Angular Language Service extension.
|
||||
3. The version of `typescript` present in the current workspace's node_modules.
|
||||
|
||||
We suggest **not** specifying `typescript.tsdk` in your VSCode settings
|
||||
per method (1) above. If the `typescript` package is loaded by
|
||||
methods (1) or (3), there is a potential for a mismatch between
|
||||
the API expected by `@angular/language-service` and the API provided by `typescript`. This could
|
||||
lead to a failure of the language service extension.
|
||||
|
||||
For more information, please see [#594](https://github.com/angular/vscode-ng-language-service/issues/594).
|
||||
|
||||
## Installing a particular release build
|
||||
|
||||
Download the `.vsix` file for the release that you want to install from the [releases](https://github.com/angular/vscode-ng-language-service/releases) tab.
|
||||
|
||||
*Do not open the .vsix file directly*. Instead, in Visual Studio code, go to the extensions tab. Click on the "..." menu in the upper right corner of the extensions tab, select "Install from vsix..." and then select the .vsix file for the release you just downloaded.
|
||||
|
||||
The extension can also be installed with the following command:
|
||||
|
||||
```
|
||||
code --install-extension /path/to/ngls.vsix
|
||||
```
|
||||
|
||||
## Angular Language Service for Other Editors
|
||||
|
||||
- [coc-angular](https://github.com/iamcco/coc-angular) for ([Neo](https://neovim.io))[vim](https://www.vim.org/)
|
||||
- [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md#angularls) for [Neovim](https://neovim.io)
|
||||
- [Wild Web Developer](https://github.com/eclipse/wildwebdeveloper) for Eclipse
|
||||
- [lsp-mode](https://github.com/emacs-lsp/lsp-mode) for Emacs
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
@@ -0,0 +1,39 @@
|
||||
load("@aspect_rules_esbuild//esbuild:defs.bzl", "esbuild")
|
||||
load("@aspect_rules_ts//ts:defs.bzl", "ts_config")
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
visibility = ["//vscode-ng-language-service/client:__subpackages__"],
|
||||
deps = [
|
||||
"//vscode-ng-language-service:tsconfig",
|
||||
"//vscode-ng-language-service/common:tsconfig",
|
||||
],
|
||||
)
|
||||
|
||||
esbuild(
|
||||
name = "index",
|
||||
config = {
|
||||
# Workaround for https://github.com/aspect-build/rules_esbuild/issues/58
|
||||
"resolveExtensions": [".js"],
|
||||
},
|
||||
entry_point = "//vscode-ng-language-service/client/src:extension.js",
|
||||
external = [
|
||||
"fs",
|
||||
"path",
|
||||
"vscode",
|
||||
"vscode-languageclient/node",
|
||||
"vscode-languageserver-protocol",
|
||||
"vscode-jsonrpc",
|
||||
],
|
||||
format = "cjs",
|
||||
# Do not enable minification. It seems to break the extension on Windows (with WSL). See #1198.
|
||||
minify = False,
|
||||
platform = "node",
|
||||
sourcemap = "external",
|
||||
visibility = ["//vscode-ng-language-service:__pkg__"],
|
||||
deps = [
|
||||
"//:node_modules/source-map-support",
|
||||
"//vscode-ng-language-service/client/src",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
|
||||
|
||||
ts_project(
|
||||
name = "src",
|
||||
srcs = glob(["*.ts"]),
|
||||
declaration = True,
|
||||
source_map = True,
|
||||
tsconfig = "//vscode-ng-language-service/client:tsconfig",
|
||||
visibility = [
|
||||
"//vscode-ng-language-service/client:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//vscode-ng-language-service:node_modules/@types/node",
|
||||
"//vscode-ng-language-service:node_modules/@types/vscode",
|
||||
"//vscode-ng-language-service:node_modules/typescript",
|
||||
"//vscode-ng-language-service:node_modules/vscode-languageclient",
|
||||
"//vscode-ng-language-service/common",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,676 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as lsp from 'vscode-languageclient/node';
|
||||
|
||||
import {
|
||||
OpenOutputChannel,
|
||||
ProjectLoadingFinish,
|
||||
ProjectLoadingStart,
|
||||
SuggestStrictMode,
|
||||
SuggestStrictModeParams,
|
||||
} from '../../common/notifications';
|
||||
import {
|
||||
GetComponentsWithTemplateFile,
|
||||
GetTcbRequest,
|
||||
GetTemplateLocationForComponent,
|
||||
IsInAngularProject,
|
||||
} from '../../common/requests';
|
||||
import {NodeModule, resolve} from '../../common/resolver';
|
||||
|
||||
import {isInsideStringLiteral, isNotTypescriptOrSupportedDecoratorField} from './embedded_support';
|
||||
|
||||
interface GetTcbResponse {
|
||||
uri: vscode.Uri;
|
||||
content: string;
|
||||
selections: vscode.Range[];
|
||||
}
|
||||
|
||||
export class AngularLanguageClient implements vscode.Disposable {
|
||||
private client: lsp.LanguageClient | null = null;
|
||||
private readonly disposables: vscode.Disposable[] = [];
|
||||
private readonly outputChannel: vscode.OutputChannel;
|
||||
private readonly clientOptions: lsp.LanguageClientOptions;
|
||||
private readonly name = 'Angular Language Service';
|
||||
private readonly virtualDocumentContents = new Map<string, string>();
|
||||
/** A map that indicates whether Angular could be found in the file's project. */
|
||||
private readonly fileToIsInAngularProjectMap = new Map<string, boolean>();
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {
|
||||
vscode.workspace.registerTextDocumentContentProvider('angular-embedded-content', {
|
||||
provideTextDocumentContent: (uri) => {
|
||||
return this.virtualDocumentContents.get(uri.toString());
|
||||
},
|
||||
});
|
||||
|
||||
this.outputChannel = vscode.window.createOutputChannel(this.name);
|
||||
// Options to control the language client
|
||||
this.clientOptions = {
|
||||
// Register the server for Angular templates and TypeScript documents
|
||||
documentSelector: [
|
||||
// scheme: 'file' means listen to changes to files on disk only
|
||||
// other option is 'untitled', for buffer in the editor (like a new doc)
|
||||
{scheme: 'file', language: 'html'},
|
||||
{scheme: 'file', language: 'typescript'},
|
||||
],
|
||||
synchronize: {
|
||||
fileEvents: [
|
||||
// Notify the server about file changes to tsconfig.json contained in the workspace
|
||||
vscode.workspace.createFileSystemWatcher('**/tsconfig.json'),
|
||||
],
|
||||
},
|
||||
// Don't let our output console pop open
|
||||
revealOutputChannelOn: lsp.RevealOutputChannelOn.Never,
|
||||
outputChannel: this.outputChannel,
|
||||
markdown: {
|
||||
isTrusted: true,
|
||||
},
|
||||
middleware: {
|
||||
provideCodeActions: async (
|
||||
document: vscode.TextDocument,
|
||||
range: vscode.Range,
|
||||
context: vscode.CodeActionContext,
|
||||
token: vscode.CancellationToken,
|
||||
next: lsp.ProvideCodeActionsSignature,
|
||||
) => {
|
||||
// Code actions can trigger also outside of `@Component(<...>)` fields.
|
||||
if (await this.isInAngularProject(document)) {
|
||||
return next(document, range, context, token);
|
||||
}
|
||||
},
|
||||
prepareRename: async (
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
token: vscode.CancellationToken,
|
||||
next: lsp.PrepareRenameSignature,
|
||||
) => {
|
||||
// We are able to provide renames for many types of string literals: template strings,
|
||||
// pipe names, and hopefully in the future selectors and input/output aliases. Because
|
||||
// TypeScript isn't able to provide renames for these, we can more or less
|
||||
// guarantee that the Angular Language service will be called for the rename as the
|
||||
// fallback. We specifically do not provide renames outside of string literals
|
||||
// because we cannot ensure our extension is prioritized for renames in TS files (see
|
||||
// https://github.com/microsoft/vscode/issues/115354) we disable renaming completely so we
|
||||
// can provide consistent expectations.
|
||||
if (
|
||||
(await this.isInAngularProject(document)) &&
|
||||
isInsideStringLiteral(document, position)
|
||||
) {
|
||||
return next(document, position, token);
|
||||
}
|
||||
},
|
||||
provideDefinition: async (
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
token: vscode.CancellationToken,
|
||||
next: lsp.ProvideDefinitionSignature,
|
||||
) => {
|
||||
if (
|
||||
(await this.isInAngularProject(document)) &&
|
||||
isNotTypescriptOrSupportedDecoratorField(document, position)
|
||||
) {
|
||||
return next(document, position, token);
|
||||
}
|
||||
},
|
||||
provideTypeDefinition: async (
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
token: vscode.CancellationToken,
|
||||
next,
|
||||
) => {
|
||||
if (
|
||||
(await this.isInAngularProject(document)) &&
|
||||
isNotTypescriptOrSupportedDecoratorField(document, position)
|
||||
) {
|
||||
return next(document, position, token);
|
||||
}
|
||||
},
|
||||
provideHover: async (
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
token: vscode.CancellationToken,
|
||||
next: lsp.ProvideHoverSignature,
|
||||
) => {
|
||||
if (
|
||||
!(await this.isInAngularProject(document)) ||
|
||||
!isNotTypescriptOrSupportedDecoratorField(document, position)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const angularResultsPromise = next(document, position, token);
|
||||
|
||||
// Include results for inline HTML via virtual document and native html providers.
|
||||
if (document.languageId === 'typescript') {
|
||||
const vdocUri = this.createVirtualHtmlDoc(document);
|
||||
const htmlProviderResultsPromise = vscode.commands.executeCommand<vscode.Hover[]>(
|
||||
'vscode.executeHoverProvider',
|
||||
vdocUri,
|
||||
position,
|
||||
);
|
||||
|
||||
const [angularResults, htmlProviderResults] = await Promise.all([
|
||||
angularResultsPromise,
|
||||
htmlProviderResultsPromise,
|
||||
]);
|
||||
return angularResults ?? htmlProviderResults?.[0];
|
||||
}
|
||||
|
||||
return angularResultsPromise;
|
||||
},
|
||||
provideSignatureHelp: async (
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
context: vscode.SignatureHelpContext,
|
||||
token: vscode.CancellationToken,
|
||||
next: lsp.ProvideSignatureHelpSignature,
|
||||
) => {
|
||||
if (
|
||||
(await this.isInAngularProject(document)) &&
|
||||
isNotTypescriptOrSupportedDecoratorField(document, position)
|
||||
) {
|
||||
return next(document, position, context, token);
|
||||
}
|
||||
},
|
||||
provideCompletionItem: async (
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
context: vscode.CompletionContext,
|
||||
token: vscode.CancellationToken,
|
||||
next: lsp.ProvideCompletionItemsSignature,
|
||||
) => {
|
||||
// If not in inline template, do not perform request forwarding
|
||||
if (
|
||||
!(await this.isInAngularProject(document)) ||
|
||||
!isNotTypescriptOrSupportedDecoratorField(document, position)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const angularCompletionsPromise = next(document, position, context, token) as Promise<
|
||||
vscode.CompletionItem[] | null | undefined
|
||||
>;
|
||||
|
||||
// Include results for inline HTML via virtual document and native html providers.
|
||||
if (document.languageId === 'typescript') {
|
||||
const vdocUri = this.createVirtualHtmlDoc(document);
|
||||
// This will not include angular stuff because the vdoc is not associated with an
|
||||
// angular component
|
||||
const htmlProviderCompletionsPromise =
|
||||
vscode.commands.executeCommand<vscode.CompletionList>(
|
||||
'vscode.executeCompletionItemProvider',
|
||||
vdocUri,
|
||||
position,
|
||||
context.triggerCharacter,
|
||||
);
|
||||
const [angularCompletions, htmlProviderCompletions] = await Promise.all([
|
||||
angularCompletionsPromise,
|
||||
htmlProviderCompletionsPromise,
|
||||
]);
|
||||
return [...(angularCompletions ?? []), ...(htmlProviderCompletions?.items ?? [])];
|
||||
}
|
||||
|
||||
return angularCompletionsPromise;
|
||||
},
|
||||
provideFoldingRanges: async (
|
||||
document: vscode.TextDocument,
|
||||
context: vscode.FoldingContext,
|
||||
token: vscode.CancellationToken,
|
||||
next,
|
||||
) => {
|
||||
if (!(await this.isInAngularProject(document))) {
|
||||
return null;
|
||||
}
|
||||
return next(document, context, token);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async applyWorkspaceEdits(workspaceEdits: lsp.WorkspaceEdit[]) {
|
||||
for (const edit of workspaceEdits) {
|
||||
const workspaceEdit = this.client?.protocol2CodeConverter.asWorkspaceEdit(edit);
|
||||
if (workspaceEdit === undefined) {
|
||||
continue;
|
||||
}
|
||||
await vscode.workspace.applyEdit(workspaceEdit);
|
||||
}
|
||||
}
|
||||
|
||||
private async isInAngularProject(doc: vscode.TextDocument): Promise<boolean> {
|
||||
if (this.client === null) {
|
||||
return false;
|
||||
}
|
||||
const uri = doc.uri.toString();
|
||||
if (this.fileToIsInAngularProjectMap.has(uri)) {
|
||||
return this.fileToIsInAngularProjectMap.get(uri)!;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.client.sendRequest(IsInAngularProject, {
|
||||
textDocument: this.client.code2ProtocolConverter.asTextDocumentIdentifier(doc),
|
||||
});
|
||||
if (response == null) {
|
||||
// If the response indicates the answer can't be determined at the moment, return `false`
|
||||
// but do not cache the result so we can try to get the real answer on follow-up requests.
|
||||
return false;
|
||||
}
|
||||
this.fileToIsInAngularProjectMap.set(uri, response);
|
||||
return response;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private createVirtualHtmlDoc(document: vscode.TextDocument): vscode.Uri {
|
||||
const originalUri = document.uri.toString();
|
||||
const vdocUri = vscode.Uri.file(encodeURIComponent(originalUri) + '.html').with({
|
||||
scheme: 'angular-embedded-content',
|
||||
authority: 'html',
|
||||
});
|
||||
this.virtualDocumentContents.set(vdocUri.toString(), document.getText());
|
||||
return vdocUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spin up the language server in a separate process and establish a connection.
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.client !== null) {
|
||||
throw new Error(`An existing client is running. Call stop() first.`);
|
||||
}
|
||||
|
||||
// Node module for the language server
|
||||
const args = await this.constructArgs();
|
||||
const prodBundle = this.context.asAbsolutePath('server');
|
||||
const devBundle = this.context.asAbsolutePath(
|
||||
path.join('bazel-bin', 'server', 'src', 'server.js'),
|
||||
);
|
||||
|
||||
// If the extension is launched in debug mode then the debug server options are used
|
||||
// Otherwise the run options are used
|
||||
const serverOptions: lsp.ServerOptions = {
|
||||
run: {
|
||||
module: this.context.asAbsolutePath('server'),
|
||||
transport: lsp.TransportKind.ipc,
|
||||
args,
|
||||
},
|
||||
debug: {
|
||||
// VS Code Insider launches extensions in debug mode by default but users
|
||||
// install prod bundle so we have to check whether dev bundle exists.
|
||||
module: fs.existsSync(devBundle) ? devBundle : prodBundle,
|
||||
transport: lsp.TransportKind.ipc,
|
||||
options: {
|
||||
// Argv options for Node.js
|
||||
execArgv: [
|
||||
// do not lazily evaluate the code so all breakpoints are respected
|
||||
'--nolazy',
|
||||
// If debugging port is changed, update .vscode/launch.json as well
|
||||
'--inspect=6009',
|
||||
],
|
||||
env: {
|
||||
NG_DEBUG: true,
|
||||
},
|
||||
},
|
||||
args,
|
||||
},
|
||||
};
|
||||
|
||||
if (!extensionVersionCompatibleWithAllProjects(serverOptions.run.module)) {
|
||||
vscode.window.showWarningMessage(
|
||||
`A project in the workspace is using a newer version of Angular than the language service extension. ` +
|
||||
`This may cause the extension to show incorrect diagnostics.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Create the language client and start the client.
|
||||
const forceDebug = process.env['NG_DEBUG'] === 'true';
|
||||
this.client = new lsp.LanguageClient(
|
||||
// This is the ID for Angular-specific configurations, like "angular.log".
|
||||
// See contributes.configuration in package.json.
|
||||
'angular',
|
||||
this.name,
|
||||
serverOptions,
|
||||
this.clientOptions,
|
||||
forceDebug,
|
||||
);
|
||||
this.disposables.push(this.client.start());
|
||||
await this.client.onReady();
|
||||
// Must wait for the client to be ready before registering notification
|
||||
// handlers.
|
||||
this.disposables.push(registerNotificationHandlers(this.client));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the arguments that's used to spawn the server process.
|
||||
* @param ctx vscode extension context
|
||||
*/
|
||||
private async constructArgs(): Promise<string[]> {
|
||||
const config = vscode.workspace.getConfiguration();
|
||||
const args: string[] = ['--logToConsole'];
|
||||
|
||||
const ngLog: string = config.get('angular.log', 'off');
|
||||
if (ngLog !== 'off') {
|
||||
// Log file does not yet exist on disk. It is up to the server to create the file.
|
||||
const logFile = path.join(this.context.logUri.fsPath, 'nglangsvc.log');
|
||||
args.push('--logFile', logFile);
|
||||
args.push('--logVerbosity', ngLog);
|
||||
}
|
||||
|
||||
const ngProbeLocations = getProbeLocations(this.context.extensionPath);
|
||||
args.push('--ngProbeLocations', ngProbeLocations.join(','));
|
||||
|
||||
const includeAutomaticOptionalChainCompletions = config.get<boolean>(
|
||||
'angular.suggest.includeAutomaticOptionalChainCompletions',
|
||||
);
|
||||
if (includeAutomaticOptionalChainCompletions) {
|
||||
args.push('--includeAutomaticOptionalChainCompletions');
|
||||
}
|
||||
|
||||
const includeCompletionsWithSnippetText = config.get<boolean>(
|
||||
'angular.suggest.includeCompletionsWithSnippetText',
|
||||
);
|
||||
if (includeCompletionsWithSnippetText) {
|
||||
args.push('--includeCompletionsWithSnippetText');
|
||||
}
|
||||
|
||||
const includeCompletionsForModuleExports = config.get<boolean>('angular.suggest.autoImports');
|
||||
args.push(
|
||||
'--includeCompletionsForModuleExports',
|
||||
includeCompletionsForModuleExports === undefined
|
||||
? 'true'
|
||||
: includeCompletionsForModuleExports.toString(),
|
||||
);
|
||||
|
||||
// Sort the versions from oldest to newest.
|
||||
const angularVersions = (await getAngularVersionsInWorkspace(this.outputChannel)).sort(
|
||||
(a, b) => (a.version.greaterThanOrEqual(b.version) ? 1 : -1),
|
||||
);
|
||||
|
||||
// Only disable block syntax if we find angular/core and every one we find does not support
|
||||
// block syntax
|
||||
if (angularVersions.length > 0) {
|
||||
const disableBlocks = angularVersions.every((v) => v.version.major < 17);
|
||||
const disableLet = angularVersions.every((v) => {
|
||||
return v.version.major < 18 || (v.version.major === 18 && v.version.minor < 1);
|
||||
});
|
||||
|
||||
if (disableBlocks) {
|
||||
args.push('--disableBlockSyntax');
|
||||
this.outputChannel.appendLine(
|
||||
`All workspace roots are using versions of Angular that do not support control flow block syntax.` +
|
||||
` Block syntax parsing in templates will be disabled.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (disableLet) {
|
||||
args.push('--disableLetSyntax');
|
||||
this.outputChannel.appendLine(
|
||||
`All workspace roots are using versions of Angular that do not support @let syntax.` +
|
||||
` @let syntax parsing in templates will be disabled.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass the earliest Angular version along to the compiler for maximum compatibility.
|
||||
if (angularVersions.length > 0) {
|
||||
args.push('--angularCoreVersion', angularVersions[0].version.toString());
|
||||
this.outputChannel.appendLine(
|
||||
`Using Angular version ${angularVersions[0].version.toString()}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const forceStrictTemplates = config.get<boolean>('angular.forceStrictTemplates');
|
||||
if (forceStrictTemplates) {
|
||||
args.push('--forceStrictTemplates');
|
||||
}
|
||||
|
||||
const suppressAngularDiagnosticCodes = config.get<string>(
|
||||
'angular.suppressAngularDiagnosticCodes',
|
||||
);
|
||||
if (suppressAngularDiagnosticCodes) {
|
||||
args.push('--suppressAngularDiagnosticCodes', suppressAngularDiagnosticCodes);
|
||||
}
|
||||
|
||||
const tsdk = config.get('typescript.tsdk', '');
|
||||
if (tsdk.trim().length > 0) {
|
||||
args.push('--tsdk', tsdk);
|
||||
}
|
||||
const tsProbeLocations = [...getProbeLocations(this.context.extensionPath)];
|
||||
args.push('--tsProbeLocations', tsProbeLocations.join(','));
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill the language client and perform some clean ups.
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (this.client === null) {
|
||||
return;
|
||||
}
|
||||
await this.client.stop();
|
||||
this.outputChannel.clear();
|
||||
this.dispose();
|
||||
this.client = null;
|
||||
this.fileToIsInAngularProjectMap.clear();
|
||||
this.virtualDocumentContents.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a template typecheck block at the current cursor location in the
|
||||
* specified editor.
|
||||
*/
|
||||
async getTcbUnderCursor(textEditor: vscode.TextEditor): Promise<GetTcbResponse | undefined> {
|
||||
if (this.client === null) {
|
||||
return undefined;
|
||||
}
|
||||
const c2pConverter = this.client.code2ProtocolConverter;
|
||||
// Craft a request by converting vscode params to LSP. The corresponding
|
||||
// response is in LSP.
|
||||
const response = await this.client.sendRequest(GetTcbRequest, {
|
||||
textDocument: c2pConverter.asTextDocumentIdentifier(textEditor.document),
|
||||
position: c2pConverter.asPosition(textEditor.selection.active),
|
||||
});
|
||||
if (response === null) {
|
||||
return undefined;
|
||||
}
|
||||
const p2cConverter = this.client.protocol2CodeConverter;
|
||||
// Convert the response from LSP back to vscode.
|
||||
return {
|
||||
uri: p2cConverter.asUri(response.uri),
|
||||
content: response.content,
|
||||
selections: p2cConverter.asRanges(response.selections),
|
||||
};
|
||||
}
|
||||
|
||||
get initializeResult(): lsp.InitializeResult | undefined {
|
||||
return this.client?.initializeResult;
|
||||
}
|
||||
|
||||
async getComponentsForOpenExternalTemplate(
|
||||
textEditor: vscode.TextEditor,
|
||||
): Promise<vscode.Location[] | undefined> {
|
||||
if (this.client === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const response = await this.client.sendRequest(GetComponentsWithTemplateFile, {
|
||||
textDocument: this.client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
textEditor.document,
|
||||
),
|
||||
});
|
||||
if (response === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const p2cConverter = this.client.protocol2CodeConverter;
|
||||
return response.map(
|
||||
(v) => new vscode.Location(p2cConverter.asUri(v.uri), p2cConverter.asRange(v.range)),
|
||||
);
|
||||
}
|
||||
|
||||
async getTemplateLocationForComponent(
|
||||
textEditor: vscode.TextEditor,
|
||||
): Promise<vscode.Location | null> {
|
||||
if (this.client === null) {
|
||||
return null;
|
||||
}
|
||||
const c2pConverter = this.client.code2ProtocolConverter;
|
||||
// Craft a request by converting vscode params to LSP. The corresponding
|
||||
// response is in LSP.
|
||||
const response = await this.client.sendRequest(GetTemplateLocationForComponent, {
|
||||
textDocument: c2pConverter.asTextDocumentIdentifier(textEditor.document),
|
||||
position: c2pConverter.asPosition(textEditor.selection.active),
|
||||
});
|
||||
if (response === null) {
|
||||
return null;
|
||||
}
|
||||
const p2cConverter = this.client.protocol2CodeConverter;
|
||||
return new vscode.Location(
|
||||
p2cConverter.asUri(response.uri),
|
||||
p2cConverter.asRange(response.range),
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (let d = this.disposables.pop(); d !== undefined; d = this.disposables.pop()) {
|
||||
d.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function registerNotificationHandlers(client: lsp.LanguageClient): vscode.Disposable {
|
||||
const disposables: vscode.Disposable[] = [];
|
||||
disposables.push(
|
||||
client.onNotification(ProjectLoadingStart, () => {
|
||||
vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Window,
|
||||
title: 'Initializing Angular language features',
|
||||
},
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
client.onNotification(ProjectLoadingFinish, resolve);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
disposables.push(
|
||||
client.onNotification(SuggestStrictMode, async (params: SuggestStrictModeParams) => {
|
||||
const config = vscode.workspace.getConfiguration();
|
||||
if (
|
||||
config.get('angular.enable-strict-mode-prompt') === false ||
|
||||
config.get('angular.forceStrictTemplates')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const openTsConfig = 'Open tsconfig.json';
|
||||
// Markdown is not generally supported in `showInformationMessage()`,
|
||||
// but links are supported. See
|
||||
// https://github.com/microsoft/vscode/issues/20595#issuecomment-281099832
|
||||
const doNotPromptAgain = 'Do not show again for this workspace';
|
||||
const selection = await vscode.window.showInformationMessage(
|
||||
'Some language features are not available. To access all features, enable ' +
|
||||
'[strictTemplates](https://angular.io/guide/angular-compiler-options#stricttemplates) in ' +
|
||||
'[angularCompilerOptions](https://angular.io/guide/angular-compiler-options).',
|
||||
openTsConfig,
|
||||
doNotPromptAgain,
|
||||
);
|
||||
if (selection === openTsConfig) {
|
||||
const document = await vscode.workspace.openTextDocument(params.configFilePath);
|
||||
vscode.window.showTextDocument(document);
|
||||
} else if (selection === doNotPromptAgain) {
|
||||
config.update(
|
||||
'angular.enable-strict-mode-prompt',
|
||||
false,
|
||||
vscode.ConfigurationTarget.Workspace,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
disposables.push(
|
||||
client.onNotification(OpenOutputChannel, () => {
|
||||
client.outputChannel.show();
|
||||
}),
|
||||
);
|
||||
|
||||
return vscode.Disposable.from(...disposables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the paths for the module that corresponds to the specified `configValue`,
|
||||
* and use the specified `bundled` as fallback if none is provided.
|
||||
* @param configName
|
||||
* @param bundled
|
||||
*/
|
||||
function getProbeLocations(bundled: string): string[] {
|
||||
const locations = [];
|
||||
// Prioritize the bundled version
|
||||
locations.push(bundled);
|
||||
// Look in workspaces currently open
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders || [];
|
||||
for (const folder of workspaceFolders) {
|
||||
locations.push(folder.uri.fsPath);
|
||||
}
|
||||
return locations;
|
||||
}
|
||||
|
||||
function extensionVersionCompatibleWithAllProjects(serverModuleLocation: string): boolean {
|
||||
const languageServiceVersion = resolve(
|
||||
'@angular/language-service',
|
||||
serverModuleLocation,
|
||||
)?.version;
|
||||
if (languageServiceVersion === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders || [];
|
||||
for (const workspaceFolder of workspaceFolders) {
|
||||
const angularCore = resolve('@angular/core', workspaceFolder.uri.fsPath);
|
||||
if (angularCore === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (!languageServiceVersion.greaterThanOrEqual(angularCore.version, 'minor')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses through the currently open VSCode workspace (i.e. all open folders)
|
||||
* and finds all `@angular/core` versions installed based on `package.json` files.
|
||||
*/
|
||||
async function getAngularVersionsInWorkspace(
|
||||
outputChannel: vscode.OutputChannel,
|
||||
): Promise<NodeModule[]> {
|
||||
const packageJsonFiles = await vscode.workspace.findFiles(
|
||||
'**/package.json',
|
||||
// Skip looking inside `node_module` folders as those contain irrelevant files.
|
||||
'**/node_modules/**',
|
||||
);
|
||||
const packageJsonRoots = packageJsonFiles.map((f) => path.dirname(f.fsPath));
|
||||
const angularCoreModules = new Set<NodeModule>();
|
||||
|
||||
outputChannel.appendLine(`package.json roots detected: ${packageJsonRoots.join(',\n ')}`);
|
||||
|
||||
for (const packageJsonRoot of packageJsonRoots) {
|
||||
const angularCore = resolve('@angular/core', packageJsonRoot);
|
||||
if (angularCore === undefined) {
|
||||
continue;
|
||||
}
|
||||
angularCoreModules.add(angularCore);
|
||||
}
|
||||
return Array.from(angularCoreModules);
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as lsp from 'vscode-languageclient/node';
|
||||
|
||||
import {
|
||||
OpenJsDocLinkCommand_Args,
|
||||
OpenJsDocLinkCommandId,
|
||||
ServerOptions,
|
||||
} from '../../common/initialize';
|
||||
|
||||
import {AngularLanguageClient} from './client';
|
||||
import {ANGULAR_SCHEME, TcbContentProvider} from './providers';
|
||||
|
||||
/**
|
||||
* Represent a vscode command with an ID and an impl function `execute`.
|
||||
*/
|
||||
type Command<T = any> =
|
||||
| {
|
||||
id: string;
|
||||
isTextEditorCommand: false;
|
||||
execute(_: T): Promise<unknown>;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
isTextEditorCommand: true;
|
||||
execute(textEditor: vscode.TextEditor): Promise<unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Restart the language server by killing the process then spanwing a new one.
|
||||
* @param client language client
|
||||
* @param context extension context for adding disposables
|
||||
*/
|
||||
function restartNgServer(client: AngularLanguageClient): Command {
|
||||
return {
|
||||
id: 'angular.restartNgServer',
|
||||
isTextEditorCommand: false,
|
||||
async execute() {
|
||||
await client.stop();
|
||||
await client.start();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the current server log file in a new editor.
|
||||
*/
|
||||
function openLogFile(client: AngularLanguageClient): Command {
|
||||
return {
|
||||
id: 'angular.openLogFile',
|
||||
isTextEditorCommand: false,
|
||||
async execute() {
|
||||
const serverOptions: ServerOptions | undefined = client.initializeResult?.serverOptions;
|
||||
if (!serverOptions?.logFile) {
|
||||
// Show a MessageItem to help users automatically update the
|
||||
// configuration option then restart the server.
|
||||
const selection = await vscode.window.showErrorMessage(
|
||||
`Angular server logging is off. Please set 'angular.log' and restart the server.`,
|
||||
'Enable logging and restart server',
|
||||
);
|
||||
if (selection) {
|
||||
const isGlobalConfig = false;
|
||||
await vscode.workspace
|
||||
.getConfiguration()
|
||||
.update('angular.log', 'verbose', isGlobalConfig);
|
||||
// Server will automatically restart because the config is changed
|
||||
}
|
||||
return;
|
||||
}
|
||||
const document = await vscode.workspace.openTextDocument(serverOptions.logFile);
|
||||
return vscode.window.showTextDocument(document);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Command getTemplateTcb displays a typecheck block for the template a user has
|
||||
* an active selection over, if any.
|
||||
* @param ngClient LSP client for the active session
|
||||
* @param context extension context to which disposables are pushed
|
||||
*/
|
||||
function getTemplateTcb(
|
||||
ngClient: AngularLanguageClient,
|
||||
context: vscode.ExtensionContext,
|
||||
): Command {
|
||||
const TCB_HIGHLIGHT_DECORATION = vscode.window.createTextEditorDecorationType({
|
||||
// See https://code.visualstudio.com/api/references/theme-color#editor-colors
|
||||
backgroundColor: new vscode.ThemeColor('editor.selectionHighlightBackground'),
|
||||
});
|
||||
|
||||
const tcbProvider = new TcbContentProvider();
|
||||
const disposable = vscode.workspace.registerTextDocumentContentProvider(
|
||||
ANGULAR_SCHEME,
|
||||
tcbProvider,
|
||||
);
|
||||
context.subscriptions.push(disposable);
|
||||
|
||||
return {
|
||||
id: 'angular.getTemplateTcb',
|
||||
isTextEditorCommand: true,
|
||||
async execute(textEditor: vscode.TextEditor) {
|
||||
tcbProvider.clear();
|
||||
const response = await ngClient.getTcbUnderCursor(textEditor);
|
||||
if (response === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
// Change the scheme of the URI from `file` to `ng` so that the document
|
||||
// content is requested from our own `TcbContentProvider`.
|
||||
const tcbUri = response.uri.with({
|
||||
scheme: ANGULAR_SCHEME,
|
||||
});
|
||||
tcbProvider.update(tcbUri, response.content);
|
||||
const editor = await vscode.window.showTextDocument(tcbUri, {
|
||||
viewColumn: vscode.ViewColumn.Beside,
|
||||
preserveFocus: true, // cursor remains in the active editor
|
||||
});
|
||||
editor.setDecorations(TCB_HIGHLIGHT_DECORATION, response.selections);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Command goToComponentWithTemplateFile finds components which reference an external template in
|
||||
* their `templateUrl`s.
|
||||
*
|
||||
* @param ngClient LSP client for the active session
|
||||
*/
|
||||
function goToComponentWithTemplateFile(ngClient: AngularLanguageClient): Command {
|
||||
return {
|
||||
id: 'angular.goToComponentWithTemplateFile',
|
||||
isTextEditorCommand: true,
|
||||
async execute(textEditor: vscode.TextEditor) {
|
||||
const locations = await ngClient.getComponentsForOpenExternalTemplate(textEditor);
|
||||
if (locations === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
vscode.commands.executeCommand(
|
||||
'editor.action.goToLocations',
|
||||
textEditor.document.uri,
|
||||
textEditor.selection.active,
|
||||
locations,
|
||||
'peek' /** what to do when there are multiple results */,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Command goToTemplateForComponent finds the template for a component.
|
||||
*
|
||||
* @param ngClient LSP client for the active session
|
||||
*/
|
||||
function goToTemplateForComponent(ngClient: AngularLanguageClient): Command {
|
||||
return {
|
||||
id: 'angular.goToTemplateForComponent',
|
||||
isTextEditorCommand: true,
|
||||
async execute(textEditor: vscode.TextEditor) {
|
||||
const location = await ngClient.getTemplateLocationForComponent(textEditor);
|
||||
if (location === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
vscode.commands.executeCommand(
|
||||
'editor.action.goToLocations',
|
||||
textEditor.document.uri,
|
||||
textEditor.selection.active,
|
||||
[location],
|
||||
'goto' /** What to do when there are multiple results (there can't be) */,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy command for opening links in jsdoc comments.
|
||||
*
|
||||
* This is needed to avoid incorrectly rewriting uris.
|
||||
*/
|
||||
function openJsDocLinkCommand(): Command<OpenJsDocLinkCommand_Args> {
|
||||
return {
|
||||
id: OpenJsDocLinkCommandId,
|
||||
isTextEditorCommand: false,
|
||||
async execute(args) {
|
||||
return await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(args.file), <
|
||||
vscode.TextDocumentShowOptions
|
||||
>{
|
||||
selection: new vscode.Range(
|
||||
new vscode.Position(args.position?.start.line ?? 0, args.position?.start.character ?? 0),
|
||||
new vscode.Position(args.position?.end.line ?? 0, args.position?.end.character ?? 0),
|
||||
),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function applyCodeActionCommand(ngClient: AngularLanguageClient): Command {
|
||||
return {
|
||||
id: 'angular.applyCompletionCodeAction',
|
||||
isTextEditorCommand: false,
|
||||
async execute(args: lsp.WorkspaceEdit[]) {
|
||||
await ngClient.applyWorkspaceEdits(args);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all supported vscode commands for the Angular extension.
|
||||
* @param client language client
|
||||
* @param context extension context for adding disposables
|
||||
*/
|
||||
export function registerCommands(
|
||||
client: AngularLanguageClient,
|
||||
context: vscode.ExtensionContext,
|
||||
): void {
|
||||
const commands: Command[] = [
|
||||
restartNgServer(client),
|
||||
openLogFile(client),
|
||||
getTemplateTcb(client, context),
|
||||
goToComponentWithTemplateFile(client),
|
||||
goToTemplateForComponent(client),
|
||||
openJsDocLinkCommand(),
|
||||
applyCodeActionCommand(client),
|
||||
];
|
||||
|
||||
for (const command of commands) {
|
||||
const disposable = command.isTextEditorCommand
|
||||
? vscode.commands.registerTextEditorCommand(command.id, command.execute)
|
||||
: vscode.commands.registerCommand(command.id, command.execute);
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
const ANGULAR_PROPERTY_ASSIGNMENTS = new Set([
|
||||
'template',
|
||||
'templateUrl',
|
||||
'styleUrls',
|
||||
'styleUrl',
|
||||
'host',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Determines if the position is inside a decorator
|
||||
* property that supports language service features.
|
||||
*/
|
||||
export function isNotTypescriptOrSupportedDecoratorField(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
): boolean {
|
||||
if (document.languageId !== 'typescript') {
|
||||
return true;
|
||||
}
|
||||
return isPropertyAssignmentToStringOrStringInArray(
|
||||
document.getText(),
|
||||
document.offsetAt(position),
|
||||
ANGULAR_PROPERTY_ASSIGNMENTS,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the position is inside a string literal. Returns `true` if the document language is
|
||||
* not TypeScript.
|
||||
*/
|
||||
export function isInsideStringLiteral(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
): boolean {
|
||||
if (document.languageId !== 'typescript') {
|
||||
return true;
|
||||
}
|
||||
const offset = document.offsetAt(position);
|
||||
const scanner = ts.createScanner(ts.ScriptTarget.ESNext, true /* skipTrivia */);
|
||||
scanner.setText(document.getText());
|
||||
|
||||
let token: ts.SyntaxKind = scanner.scan();
|
||||
while (token !== ts.SyntaxKind.EndOfFileToken && scanner.getStartPos() < offset) {
|
||||
const isStringToken =
|
||||
token === ts.SyntaxKind.StringLiteral ||
|
||||
token === ts.SyntaxKind.NoSubstitutionTemplateLiteral;
|
||||
const isCursorInToken =
|
||||
scanner.getStartPos() <= offset &&
|
||||
scanner.getStartPos() + scanner.getTokenText().length >= offset;
|
||||
if (isCursorInToken && isStringToken) {
|
||||
return true;
|
||||
}
|
||||
token = scanner.scan();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic scanner to determine if we're inside a string of a property with one of the given names.
|
||||
*
|
||||
* This scanner is not currently robust or perfect but provides us with an accurate answer _most_ of
|
||||
* the time.
|
||||
*
|
||||
* False positives are OK here. Though this will give some false positives for determining if a
|
||||
* position is within an Angular context, i.e. an object like `{template: ''}` that is not inside an
|
||||
* `@Component` or `{styleUrls: [someFunction('stringL¦iteral')]}`, the @angular/language-service
|
||||
* will always give us the correct answer. This helper gives us a quick win for optimizing the
|
||||
* number of requests we send to the server.
|
||||
*
|
||||
* TODO(atscott): tagged templates don't work: #1872 /
|
||||
* https://github.com/Microsoft/TypeScript/issues/20055
|
||||
*/
|
||||
function isPropertyAssignmentToStringOrStringInArray(
|
||||
documentText: string,
|
||||
offset: number,
|
||||
propertyAssignmentNames: Set<string>,
|
||||
): boolean {
|
||||
const scanner = ts.createScanner(ts.ScriptTarget.ESNext, true /* skipTrivia */);
|
||||
scanner.setText(documentText);
|
||||
|
||||
let token: ts.SyntaxKind = scanner.scan();
|
||||
let lastToken: ts.SyntaxKind | undefined;
|
||||
let lastTokenText: string | undefined;
|
||||
let unclosedBraces = 0;
|
||||
let unclosedBrackets = 0;
|
||||
let propertyAssignmentContext = false;
|
||||
while (token !== ts.SyntaxKind.EndOfFileToken && scanner.getStartPos() < offset) {
|
||||
if (
|
||||
lastToken === ts.SyntaxKind.Identifier &&
|
||||
lastTokenText !== undefined &&
|
||||
token === ts.SyntaxKind.ColonToken &&
|
||||
propertyAssignmentNames.has(lastTokenText)
|
||||
) {
|
||||
propertyAssignmentContext = true;
|
||||
token = scanner.scan();
|
||||
continue;
|
||||
}
|
||||
if (unclosedBraces === 0 && unclosedBrackets === 0 && isPropertyAssignmentTerminator(token)) {
|
||||
propertyAssignmentContext = false;
|
||||
}
|
||||
|
||||
if (token === ts.SyntaxKind.OpenBracketToken) {
|
||||
unclosedBrackets++;
|
||||
} else if (token === ts.SyntaxKind.OpenBraceToken) {
|
||||
unclosedBraces++;
|
||||
} else if (token === ts.SyntaxKind.CloseBracketToken) {
|
||||
unclosedBrackets--;
|
||||
} else if (token === ts.SyntaxKind.CloseBraceToken) {
|
||||
unclosedBraces--;
|
||||
}
|
||||
|
||||
const isStringToken =
|
||||
token === ts.SyntaxKind.StringLiteral ||
|
||||
token === ts.SyntaxKind.NoSubstitutionTemplateLiteral;
|
||||
const isCursorInToken =
|
||||
scanner.getStartPos() <= offset &&
|
||||
scanner.getStartPos() + scanner.getTokenText().length >= offset;
|
||||
if (propertyAssignmentContext && isCursorInToken && isStringToken) {
|
||||
return true;
|
||||
}
|
||||
|
||||
lastTokenText = scanner.getTokenText();
|
||||
lastToken = token;
|
||||
token = scanner.scan();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isPropertyAssignmentTerminator(token: ts.SyntaxKind) {
|
||||
return (
|
||||
token === ts.SyntaxKind.EndOfFileToken ||
|
||||
token === ts.SyntaxKind.CommaToken ||
|
||||
token === ts.SyntaxKind.SemicolonToken ||
|
||||
token === ts.SyntaxKind.CloseBraceToken
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import {AngularLanguageClient} from './client';
|
||||
import {registerCommands} from './commands';
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
const client = new AngularLanguageClient(context);
|
||||
|
||||
// Push the disposable to the context's subscriptions so that the
|
||||
// client can be deactivated on extension deactivation
|
||||
registerCommands(client, context);
|
||||
|
||||
// Restart the server on configuration change.
|
||||
const disposable = vscode.workspace.onDidChangeConfiguration(
|
||||
async (e: vscode.ConfigurationChangeEvent) => {
|
||||
if (!e.affectsConfiguration('angular')) {
|
||||
return;
|
||||
}
|
||||
await client.stop();
|
||||
await client.start();
|
||||
},
|
||||
);
|
||||
context.subscriptions.push(client, disposable);
|
||||
|
||||
client.start();
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export const ANGULAR_SCHEME = 'ng';
|
||||
|
||||
/**
|
||||
* Allocate a provider of documents corresponding to the `ng` URI scheme,
|
||||
* which we will use to provide a virtual document with the TCB contents.
|
||||
*
|
||||
* We use a virtual document provider rather than opening an untitled file to
|
||||
* ensure the buffer remains readonly (https://github.com/microsoft/vscode/issues/4873).
|
||||
*/
|
||||
export class TcbContentProvider implements vscode.TextDocumentContentProvider {
|
||||
/**
|
||||
* Event emitter used to notify VSCode of a change to the TCB virtual document,
|
||||
* prompting it to re-evaluate the document content. This is needed to bust
|
||||
* VSCode's document cache if someone requests a TCB that was previously opened.
|
||||
* https://code.visualstudio.com/api/extension-guides/virtual-documents#update-virtual-documents
|
||||
*/
|
||||
private readonly onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>();
|
||||
/**
|
||||
* Name of the typecheck file.
|
||||
*/
|
||||
private tcbFile: vscode.Uri | null = null;
|
||||
/**
|
||||
* Content of the entire typecheck file.
|
||||
*/
|
||||
private tcbContent: string | null = null;
|
||||
|
||||
/**
|
||||
* This callback is invoked only when user explicitly requests to view or
|
||||
* update typecheck file. We do not automatically update the typecheck document
|
||||
* when the source file changes.
|
||||
*/
|
||||
readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
|
||||
provideTextDocumentContent(
|
||||
uri: vscode.Uri,
|
||||
token: vscode.CancellationToken,
|
||||
): vscode.ProviderResult<string> {
|
||||
if (uri.toString() !== this.tcbFile?.toString()) {
|
||||
return null;
|
||||
}
|
||||
return this.tcbContent;
|
||||
}
|
||||
|
||||
update(uri: vscode.Uri, content: string) {
|
||||
this.tcbFile = uri;
|
||||
this.tcbContent = content;
|
||||
this.onDidChangeEmitter.fire(uri);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.tcbFile = null;
|
||||
this.tcbContent = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
load("@aspect_rules_jasmine//jasmine:defs.bzl", "jasmine_test")
|
||||
load("@aspect_rules_ts//ts:defs.bzl", "ts_config", "ts_project")
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
deps = [
|
||||
"//vscode-ng-language-service:tsconfig",
|
||||
"//vscode-ng-language-service/client:tsconfig",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "tests",
|
||||
srcs = glob(["*.ts"]),
|
||||
declaration = True,
|
||||
source_map = True,
|
||||
tsconfig = ":tsconfig",
|
||||
deps = [
|
||||
"//vscode-ng-language-service:node_modules/@types/jasmine",
|
||||
"//vscode-ng-language-service:node_modules/@types/vscode",
|
||||
"//vscode-ng-language-service:node_modules/vscode-languageserver-textdocument",
|
||||
"//vscode-ng-language-service/client/src",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_test(
|
||||
name = "test",
|
||||
args = ["*_spec.js"],
|
||||
chdir = package_name(),
|
||||
data = [":tests"],
|
||||
node_modules = "//vscode-ng-language-service:node_modules",
|
||||
tags = ["unit_test"],
|
||||
)
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import {DocumentUri, TextDocument} from 'vscode-languageserver-textdocument';
|
||||
|
||||
import {isNotTypescriptOrSupportedDecoratorField} from '../embedded_support';
|
||||
|
||||
describe('embedded language support', () => {
|
||||
describe('isInsideAngularContext', () => {
|
||||
it('empty file', () => {
|
||||
test('¦', isNotTypescriptOrSupportedDecoratorField, false);
|
||||
});
|
||||
|
||||
it('just after template', () => {
|
||||
test(
|
||||
`const foo = {template: '<div></div>'¦}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside template', () => {
|
||||
test(
|
||||
`const foo = {template: '<div>¦</div>'}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('just after templateUrl', () => {
|
||||
test(
|
||||
`const foo = {templateUrl: './abc.html'¦}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside templateUrl', () => {
|
||||
test(
|
||||
`const foo = {templateUrl: './abc¦.html'}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('just after styleUrls', () => {
|
||||
test(
|
||||
`const foo = {styleUrls: ['./abc.css']¦}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside first item of styleUrls', () => {
|
||||
test(
|
||||
`const foo = {styleUrls: ['./abc.c¦ss', 'def.css']}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside second item of styleUrls', () => {
|
||||
test(
|
||||
`const foo = {styleUrls: ['./abc.css', 'def¦.css']}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside second item of styleUrls, when first is complicated function', () => {
|
||||
test(
|
||||
`const foo = {styleUrls: [getCss({strict: true, dirs: ['apple', 'banana']}), 'def¦.css']}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside non-string item of styleUrls', () => {
|
||||
test(
|
||||
`const foo = {styleUrls: [getCss({strict: true¦, dirs: ['apple', 'banana']}), 'def.css']}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function test(
|
||||
fileWithCursor: string,
|
||||
testFn: (doc: vscode.TextDocument, position: vscode.Position) => boolean,
|
||||
expectation: boolean,
|
||||
): void {
|
||||
const {cursor, text} = extractCursorInfo(fileWithCursor);
|
||||
|
||||
const vdoc = TextDocument.create(
|
||||
'test.ts' as DocumentUri,
|
||||
'typescript',
|
||||
0,
|
||||
text,
|
||||
) as {} as vscode.TextDocument;
|
||||
(vdoc as any).fileName = 'test.ts';
|
||||
const actual = testFn(vdoc, vdoc.positionAt(cursor));
|
||||
expect(actual).toBe(expectation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a text snippet which contains exactly one cursor symbol ('¦'), extract both the offset of
|
||||
* that cursor within the text as well as the text snippet without the cursor.
|
||||
*/
|
||||
function extractCursorInfo(textWithCursor: string): {cursor: number; text: string} {
|
||||
const cursor = textWithCursor.indexOf('¦');
|
||||
if (cursor === -1 || textWithCursor.indexOf('¦', cursor + 1) !== -1) {
|
||||
throw new Error(`Expected to find exactly one cursor symbol '¦'`);
|
||||
}
|
||||
|
||||
return {
|
||||
cursor,
|
||||
text: textWithCursor.substr(0, cursor) + textWithCursor.substr(cursor + 1),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"root":["./embedded_support_spec.ts"],"version":"5.9.2"}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"rootDirs": [
|
||||
".",
|
||||
"../dist"
|
||||
]
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
load("@aspect_rules_ts//ts:defs.bzl", "ts_config", "ts_project")
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
visibility = [
|
||||
"//vscode-ng-language-service/client:__subpackages__",
|
||||
"//vscode-ng-language-service/common:__subpackages__",
|
||||
"//vscode-ng-language-service/integration:__subpackages__",
|
||||
"//vscode-ng-language-service/server:__subpackages__",
|
||||
],
|
||||
deps = ["//vscode-ng-language-service:tsconfig"],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "common",
|
||||
srcs = glob(["*.ts"]),
|
||||
declaration = True,
|
||||
source_map = True,
|
||||
tsconfig = ":tsconfig",
|
||||
visibility = [
|
||||
"//vscode-ng-language-service/client:__subpackages__",
|
||||
"//vscode-ng-language-service/common:__subpackages__",
|
||||
"//vscode-ng-language-service/integration:__subpackages__",
|
||||
"//vscode-ng-language-service/server:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//vscode-ng-language-service:node_modules/@types/node",
|
||||
"//vscode-ng-language-service:node_modules/vscode-jsonrpc",
|
||||
"//vscode-ng-language-service:node_modules/vscode-languageserver-protocol",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export interface ServerOptions {
|
||||
logFile?: string;
|
||||
}
|
||||
|
||||
export interface OpenJsDocLinkCommand_Args {
|
||||
readonly file: string;
|
||||
readonly position?: {
|
||||
start: {character: number; line: number};
|
||||
end: {character: number; line: number};
|
||||
};
|
||||
}
|
||||
|
||||
export const OpenJsDocLinkCommandId = 'angular.openJsDocLink';
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {NotificationType, NotificationType0} from 'vscode-jsonrpc';
|
||||
|
||||
export const ProjectLoadingStart = new NotificationType0('angular/projectLoadingStart');
|
||||
export const ProjectLoadingFinish = new NotificationType0('angular/projectLoadingFinish');
|
||||
|
||||
export interface ProjectLanguageServiceParams {
|
||||
projectName: string;
|
||||
languageServiceEnabled: boolean;
|
||||
}
|
||||
|
||||
export const ProjectLanguageService = new NotificationType<ProjectLanguageServiceParams>(
|
||||
'angular/projectLanguageService',
|
||||
);
|
||||
|
||||
export interface SuggestStrictModeParams {
|
||||
configFilePath: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const SuggestStrictMode = new NotificationType<SuggestStrictModeParams>(
|
||||
'angular/suggestStrictMode',
|
||||
);
|
||||
|
||||
export const OpenOutputChannel = new NotificationType('angular/OpenOutputChannel');
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as lsp from 'vscode-languageserver-protocol';
|
||||
|
||||
export const GetComponentsWithTemplateFile = new lsp.RequestType<
|
||||
GetComponentsWithTemplateFileParams,
|
||||
lsp.Location[],
|
||||
/* error */ void
|
||||
>('angular/getComponentsWithTemplateFile');
|
||||
|
||||
export interface GetComponentsWithTemplateFileParams {
|
||||
textDocument: lsp.TextDocumentIdentifier;
|
||||
}
|
||||
|
||||
export const GetTemplateLocationForComponent = new lsp.RequestType<
|
||||
GetTemplateLocationForComponentParams,
|
||||
lsp.Location,
|
||||
/* error */ void
|
||||
>('angular/getTemplateLocationForComponent');
|
||||
|
||||
export interface GetTemplateLocationForComponentParams {
|
||||
textDocument: lsp.TextDocumentIdentifier;
|
||||
position: lsp.Position;
|
||||
}
|
||||
|
||||
export interface GetTcbParams {
|
||||
textDocument: lsp.TextDocumentIdentifier;
|
||||
position: lsp.Position;
|
||||
}
|
||||
|
||||
export const GetTcbRequest = new lsp.RequestType<
|
||||
GetTcbParams,
|
||||
GetTcbResponse | null,
|
||||
/* error */ void
|
||||
>('angular/getTcb');
|
||||
|
||||
export interface GetTcbResponse {
|
||||
uri: lsp.DocumentUri;
|
||||
content: string;
|
||||
selections: lsp.Range[];
|
||||
}
|
||||
|
||||
export const IsInAngularProject = new lsp.RequestType<
|
||||
IsInAngularProjectParams,
|
||||
boolean | null,
|
||||
/* error */ void
|
||||
>('angular/isAngularCoreInOwningProject');
|
||||
|
||||
export interface IsInAngularProjectParams {
|
||||
textDocument: lsp.TextDocumentIdentifier;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
/**
|
||||
* Represents a valid node module that has been successfully resolved.
|
||||
*/
|
||||
export interface NodeModule {
|
||||
name: string;
|
||||
resolvedPath: string;
|
||||
version: Version;
|
||||
}
|
||||
|
||||
export function resolve(
|
||||
packageName: string,
|
||||
location: string,
|
||||
rootPackage?: string,
|
||||
): NodeModule | undefined {
|
||||
rootPackage = rootPackage || packageName;
|
||||
try {
|
||||
const packageJsonPath = require.resolve(`${rootPackage}/package.json`, {
|
||||
paths: [location],
|
||||
});
|
||||
// Do not use require() to read JSON files since it's a potential security
|
||||
// vulnerability.
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
const resolvedPath = require.resolve(packageName, {
|
||||
paths: [location],
|
||||
});
|
||||
return {
|
||||
name: packageName,
|
||||
resolvedPath,
|
||||
version: new Version(packageJson.version),
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export class Version {
|
||||
readonly major: number;
|
||||
readonly minor: number;
|
||||
readonly patch: number;
|
||||
|
||||
constructor(private readonly versionStr: string) {
|
||||
const [major, minor, patch] = Version.parseVersionStr(versionStr);
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
this.patch = patch;
|
||||
}
|
||||
|
||||
greaterThanOrEqual(other: Version, compare: 'patch' | 'minor' | 'major' = 'patch'): boolean {
|
||||
if (this.major < other.major) {
|
||||
return false;
|
||||
}
|
||||
if (this.major > other.major || (this.major === other.major && compare === 'major')) {
|
||||
return true;
|
||||
}
|
||||
if (this.minor < other.minor) {
|
||||
return false;
|
||||
}
|
||||
if (this.minor > other.minor || (this.minor === other.minor && compare === 'minor')) {
|
||||
return true;
|
||||
}
|
||||
return this.patch >= other.patch;
|
||||
}
|
||||
|
||||
isVersionZero() {
|
||||
return this.versionStr === '0.0.0';
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.versionStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the specified `versionStr` to its number constituents. Invalid
|
||||
* number value is represented as negative number.
|
||||
* @param versionStr
|
||||
*/
|
||||
static parseVersionStr(versionStr: string): [number, number, number] {
|
||||
const [major, minor, patch] = versionStr.split('.').map(parseNonNegativeInt);
|
||||
return [
|
||||
major === undefined ? 0 : major,
|
||||
minor === undefined ? 0 : minor,
|
||||
patch === undefined ? 0 : patch,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the specified string `a` to non-negative integer.
|
||||
* Returns -1 if the result is NaN.
|
||||
* @param a
|
||||
*/
|
||||
function parseNonNegativeInt(a: string): number {
|
||||
// parseInt() will try to convert as many as possible leading characters that
|
||||
// are digits. This means a string like "123abc" will be converted to 123.
|
||||
// For our use case, this is sufficient.
|
||||
const i = parseInt(a, 10 /* radix */);
|
||||
return isNaN(i) ? -1 : i;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
load("@aspect_rules_jasmine//jasmine:defs.bzl", "jasmine_test")
|
||||
load("@aspect_rules_ts//ts:defs.bzl", "ts_config", "ts_project")
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
deps = [
|
||||
"//vscode-ng-language-service:tsconfig",
|
||||
"//vscode-ng-language-service/common:tsconfig",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "tests",
|
||||
srcs = glob(["*.ts"]),
|
||||
declaration = True,
|
||||
source_map = True,
|
||||
tsconfig = ":tsconfig",
|
||||
deps = [
|
||||
"//vscode-ng-language-service:node_modules/@types/jasmine",
|
||||
"//vscode-ng-language-service/common",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_test(
|
||||
name = "test",
|
||||
args = ["*_spec.js"],
|
||||
chdir = package_name(),
|
||||
data = [":tests"],
|
||||
node_modules = "//vscode-ng-language-service:node_modules",
|
||||
tags = ["unit_test"],
|
||||
)
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Version} from '../resolver';
|
||||
|
||||
describe('Version', () => {
|
||||
it('should parse version string correctly', () => {
|
||||
const cases: Array<[string, number, number, number]> = [
|
||||
// version string | major | minor | patch
|
||||
['1', 1, 0, 0],
|
||||
['1.2', 1, 2, 0],
|
||||
['1.2.3', 1, 2, 3],
|
||||
['9.0.0-rc.1+126.sha-0c38aae.with-local-changes', 9, 0, 0],
|
||||
];
|
||||
for (const [versionStr, major, minor, patch] of cases) {
|
||||
const v = new Version(versionStr);
|
||||
expect(v.major).toBe(major);
|
||||
expect(v.minor).toBe(minor);
|
||||
expect(v.patch).toBe(patch);
|
||||
}
|
||||
});
|
||||
|
||||
it('should compare versions correctly', () => {
|
||||
const cases: Array<[string, string, boolean]> = [
|
||||
// lhs | rhs | result
|
||||
['1', '1', true],
|
||||
['1', '2', false],
|
||||
['2', '2.0', true],
|
||||
['2', '2.1', false],
|
||||
['2', '2.0.0', true],
|
||||
['2', '2.0.1', false],
|
||||
|
||||
['1.2', '1', true],
|
||||
['1.2', '2', false],
|
||||
['2.2', '2.1', true],
|
||||
['2.2', '2.7', false],
|
||||
['3.2', '3.2.0', true],
|
||||
['3.2', '3.2.1', false],
|
||||
|
||||
['1.2.3', '1', true],
|
||||
['1.2.3', '2', false],
|
||||
['2.2.3', '2.1', true],
|
||||
['2.2.3', '2.3', false],
|
||||
['3.2.3', '3.2.2', true],
|
||||
['3.2.3', '3.2.4', false],
|
||||
];
|
||||
for (const [s1, s2, result] of cases) {
|
||||
const v1 = new Version(s1);
|
||||
const v2 = new Version(s2);
|
||||
expect(v1.greaterThanOrEqual(v2)).toBe(result, `Expect ${v1} >= ${v2}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"root":["./resolver_spec.ts"],"version":"5.9.2"}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
load("@aspect_rules_ts//ts:defs.bzl", "ts_config", "ts_project")
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
visibility = ["//vscode-ng-language-service/integration:__subpackages__"],
|
||||
deps = [
|
||||
"//vscode-ng-language-service:tsconfig",
|
||||
"//vscode-ng-language-service/server:tsconfig",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "integration",
|
||||
# NB: there is an import cycle between integration/lsp/*.ts and integration/test_constants.ts so
|
||||
# they cannot be broken up into separate ts_project targets
|
||||
srcs = glob([
|
||||
"*.ts",
|
||||
]) + ["//vscode-ng-language-service/integration/lsp:srcs"],
|
||||
declaration = True,
|
||||
source_map = True,
|
||||
tsconfig = ":tsconfig",
|
||||
visibility = [
|
||||
"//vscode-ng-language-service/integration:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//vscode-ng-language-service:node_modules/@types/jasmine",
|
||||
"//vscode-ng-language-service:node_modules/@types/node",
|
||||
"//vscode-ng-language-service:node_modules/vscode-jsonrpc",
|
||||
"//vscode-ng-language-service:node_modules/vscode-languageserver-protocol",
|
||||
"//vscode-ng-language-service:node_modules/vscode-uri",
|
||||
"//vscode-ng-language-service/common",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
load("@aspect_rules_js//js:defs.bzl", "js_test")
|
||||
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
|
||||
|
||||
ts_project(
|
||||
name = "e2e",
|
||||
srcs = glob(["*.ts"]),
|
||||
declaration = True,
|
||||
source_map = True,
|
||||
tsconfig = "//vscode-ng-language-service/integration:tsconfig",
|
||||
visibility = [
|
||||
"//vscode-ng-language-service/integration:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//vscode-ng-language-service:node_modules/@types/jasmine",
|
||||
"//vscode-ng-language-service:node_modules/@types/node",
|
||||
"//vscode-ng-language-service:node_modules/@types/vscode",
|
||||
"//vscode-ng-language-service:node_modules/vscode-test",
|
||||
"//vscode-ng-language-service/integration",
|
||||
],
|
||||
)
|
||||
|
||||
js_test(
|
||||
name = "test",
|
||||
data = [
|
||||
":e2e",
|
||||
"//vscode-ng-language-service:npm",
|
||||
"//vscode-ng-language-service/integration",
|
||||
"//vscode-ng-language-service/integration/project",
|
||||
# Depend on //:node_modules/jasmine as a temporary work-around for jasmine escaping its
|
||||
# runfiles likely due to esm import issue in rules_js https://github.com/aspect-build/rules_js/issues/362
|
||||
"//vscode-ng-language-service:node_modules/jasmine",
|
||||
],
|
||||
entry_point = ":index.js",
|
||||
# This test downloads vscode & installs it which is not allowed in the sandbox
|
||||
# due to file system protection. It is tagged local so it runs outside of
|
||||
# the sandbox. It would need some redesign to allow for sandbox execution.
|
||||
# Tagged manual so that `bazel test //...` doesn't run it automatically.
|
||||
tags = [
|
||||
"e2e",
|
||||
"local",
|
||||
"manual",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import {activate, COMPLETION_COMMAND, FOO_TEMPLATE_URI} from './helper';
|
||||
|
||||
describe('Angular Ivy LS completions', () => {
|
||||
beforeAll(async () => {
|
||||
await activate(FOO_TEMPLATE_URI);
|
||||
});
|
||||
|
||||
it(`does not duplicate HTML completions in external templates`, async () => {
|
||||
const position = new vscode.Position(0, 0);
|
||||
const completionItem = await vscode.commands.executeCommand<vscode.CompletionList>(
|
||||
COMPLETION_COMMAND,
|
||||
FOO_TEMPLATE_URI,
|
||||
position,
|
||||
);
|
||||
const regionCompletions = completionItem?.items?.filter((i) => i.label === '#region') ?? [];
|
||||
expect(regionCompletions.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import {APP_COMPONENT} from '../test_constants';
|
||||
|
||||
import {activate} from './helper';
|
||||
|
||||
const DEFINITION_COMMAND = 'vscode.executeDefinitionProvider';
|
||||
const APP_COMPONENT_URI = vscode.Uri.file(APP_COMPONENT);
|
||||
|
||||
describe('Angular Ivy LS', () => {
|
||||
beforeAll(async () => {
|
||||
await activate(APP_COMPONENT_URI);
|
||||
}, 25000 /* 25 seconds */);
|
||||
|
||||
it(`returns definition for variable in template`, async () => {
|
||||
// vscode Position is zero-based
|
||||
// template: `<h1>Hello {{name}}</h1>`,
|
||||
// ^-------- here
|
||||
const position = new vscode.Position(4, 25);
|
||||
// For a complete list of standard commands, see
|
||||
// https://code.visualstudio.com/api/references/commands
|
||||
const definitions = await vscode.commands.executeCommand<vscode.LocationLink[]>(
|
||||
DEFINITION_COMMAND,
|
||||
APP_COMPONENT_URI,
|
||||
position,
|
||||
);
|
||||
expect(definitions?.length).toBe(1);
|
||||
const def = definitions![0];
|
||||
expect(def.targetUri.fsPath).toBe(APP_COMPONENT); // in the same document
|
||||
const {start, end} = def.targetRange;
|
||||
// Should start and end on line 6
|
||||
expect(start.line).toBe(7);
|
||||
expect(end.line).toBe(7);
|
||||
expect(start.character).toBe(2);
|
||||
expect(end.character).toBe(start.character + `name`.length);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import {APP_COMPONENT, FOO_TEMPLATE} from '../test_constants';
|
||||
|
||||
export const COMPLETION_COMMAND = 'vscode.executeCompletionItemProvider';
|
||||
export const HOVER_COMMAND = 'vscode.executeHoverProvider';
|
||||
export const DEFINITION_COMMAND = 'vscode.executeDefinitionProvider';
|
||||
export const APP_COMPONENT_URI = vscode.Uri.file(APP_COMPONENT);
|
||||
export const FOO_TEMPLATE_URI = vscode.Uri.file(FOO_TEMPLATE);
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export async function activate(uri: vscode.Uri) {
|
||||
// set default timeout to 30 seconds
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30_000;
|
||||
await vscode.window.showTextDocument(uri);
|
||||
await waitForDefinitionsToBeAvailable(20);
|
||||
}
|
||||
|
||||
async function waitForDefinitionsToBeAvailable(maxSeconds: number) {
|
||||
let tries = 0;
|
||||
while (tries < maxSeconds) {
|
||||
const position = new vscode.Position(4, 25);
|
||||
// For a complete list of standard commands, see
|
||||
// https://code.visualstudio.com/api/references/commands
|
||||
const definitions = await vscode.commands.executeCommand<vscode.LocationLink[]>(
|
||||
DEFINITION_COMMAND,
|
||||
APP_COMPONENT_URI,
|
||||
position,
|
||||
);
|
||||
if (definitions && definitions.length > 0) {
|
||||
return;
|
||||
}
|
||||
tries++;
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import {activate, FOO_TEMPLATE_URI, HOVER_COMMAND} from './helper';
|
||||
|
||||
// This hover tests appear to be the only flaky ones in the suite. Disable until they can
|
||||
// consistently pass.
|
||||
xdescribe('Angular Ivy LS quick info', () => {
|
||||
beforeAll(async () => {
|
||||
await activate(FOO_TEMPLATE_URI);
|
||||
});
|
||||
|
||||
it(`returns quick info from built in extension for class in template`, async () => {
|
||||
const position = new vscode.Position(1, 8);
|
||||
const quickInfo = await vscode.commands.executeCommand<vscode.Hover[]>(
|
||||
HOVER_COMMAND,
|
||||
FOO_TEMPLATE_URI,
|
||||
position,
|
||||
);
|
||||
expect(quickInfo?.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import {join} from 'path';
|
||||
import {runTests} from 'vscode-test';
|
||||
|
||||
import {IS_BAZEL, PACKAGE_ROOT, PROJECT_PATH} from '../test_constants';
|
||||
|
||||
async function main() {
|
||||
const EXT_DEVELOPMENT_PATH = IS_BAZEL
|
||||
? join(PACKAGE_ROOT, 'npm')
|
||||
: join(PACKAGE_ROOT, 'dist', 'npm');
|
||||
const EXT_TESTS_PATH = IS_BAZEL
|
||||
? join(PACKAGE_ROOT, 'integration', 'e2e', 'jasmine')
|
||||
: join(PACKAGE_ROOT, 'dist', 'integration', 'e2e', 'jasmine');
|
||||
|
||||
try {
|
||||
await runTests({
|
||||
// Keep version in sync with vscode engine version in package.json
|
||||
version: '1.74.3',
|
||||
extensionDevelopmentPath: EXT_DEVELOPMENT_PATH,
|
||||
extensionTestsPath: EXT_TESTS_PATH,
|
||||
launchArgs: [
|
||||
PROJECT_PATH,
|
||||
// This disables all extensions except the one being tested
|
||||
'--disable-extensions',
|
||||
],
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to run tests', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,44 @@
|
||||
import Jasmine = require('jasmine');
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
const jasmine = new Jasmine({projectBaseDir: __dirname});
|
||||
|
||||
jasmine.loadConfig({
|
||||
spec_files: ['*_spec.js'],
|
||||
});
|
||||
|
||||
// For whatever reason, the built-in jasmine reporter printin does not make it to the console
|
||||
// output when the tests are run. In addition, allowing the default implementation to call
|
||||
// `process.exit(1)` messes up the console reporting. The overrides below allow for both the
|
||||
// proper exit code and the proper console reporting.
|
||||
let failed = false;
|
||||
jasmine.configureDefaultReporter({
|
||||
// The `print` function passed the reporter will be called to print its results.
|
||||
print: function (message: string) {
|
||||
if (message.trim()) {
|
||||
console.log(message);
|
||||
}
|
||||
},
|
||||
});
|
||||
jasmine.completionReporter = {
|
||||
specDone: (result: jasmine.SpecResult): void | Promise<void> => {
|
||||
if (result.failedExpectations.length > 0) {
|
||||
failed = true;
|
||||
}
|
||||
console.log(result);
|
||||
},
|
||||
};
|
||||
|
||||
console.log(`Expecting to run ${jasmine.specFiles.length} specs.`);
|
||||
|
||||
console.log(JSON.stringify(jasmine.specFiles, null, 2));
|
||||
|
||||
if (jasmine.specFiles.length === 0) {
|
||||
throw new Error('No specs found');
|
||||
}
|
||||
|
||||
await jasmine.execute();
|
||||
if (failed) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
load("@aspect_bazel_lib//lib:copy_to_bin.bzl", "copy_to_bin")
|
||||
load("@aspect_rules_jasmine//jasmine:defs.bzl", "jasmine_test")
|
||||
|
||||
jasmine_test(
|
||||
name = "test",
|
||||
args = ["*_spec.js"],
|
||||
chdir = package_name(),
|
||||
data = [
|
||||
"//vscode-ng-language-service/integration",
|
||||
"//vscode-ng-language-service/integration/pre_standalone_project",
|
||||
"//vscode-ng-language-service/integration/pre_standalone_project:node_modules/@angular/common",
|
||||
"//vscode-ng-language-service/integration/pre_standalone_project:node_modules/@angular/core",
|
||||
"//vscode-ng-language-service/integration/project",
|
||||
"//vscode-ng-language-service/integration/project:node_modules/@angular/common",
|
||||
"//vscode-ng-language-service/integration/project:node_modules/@angular/core",
|
||||
"//vscode-ng-language-service/server:index",
|
||||
],
|
||||
node_modules = "//vscode-ng-language-service:node_modules",
|
||||
shard_count = 2,
|
||||
tags = [
|
||||
"e2e",
|
||||
"no-remote",
|
||||
"no-sandbox",
|
||||
],
|
||||
)
|
||||
|
||||
copy_to_bin(
|
||||
name = "srcs",
|
||||
srcs = glob(["*.ts"]),
|
||||
# NB: there is an import cycle between integration/lsp/*.ts and integration/test_constants.ts so
|
||||
# they cannot be broken up into separate ts_project targets
|
||||
visibility = ["//vscode-ng-language-service/integration:__subpackages__"],
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {fork} from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import {
|
||||
createMessageConnection,
|
||||
IPCMessageReader,
|
||||
IPCMessageWriter,
|
||||
MessageConnection,
|
||||
} from 'vscode-jsonrpc/node';
|
||||
import * as lsp from 'vscode-languageserver-protocol';
|
||||
import {URI} from 'vscode-uri';
|
||||
|
||||
import {PROJECT_PATH, SERVER_PATH} from '../test_constants';
|
||||
|
||||
export interface ServerOptions {
|
||||
ivy: boolean;
|
||||
includeAutomaticOptionalChainCompletions?: boolean;
|
||||
includeCompletionsWithSnippetText?: boolean;
|
||||
angularCoreVersion?: string;
|
||||
}
|
||||
|
||||
export function createConnection(serverOptions: ServerOptions): MessageConnection {
|
||||
const argv: string[] = [
|
||||
'--node-ipc',
|
||||
'--tsProbeLocations',
|
||||
SERVER_PATH,
|
||||
'--ngProbeLocations',
|
||||
[SERVER_PATH, PROJECT_PATH].join(','),
|
||||
];
|
||||
if (!serverOptions.ivy) {
|
||||
argv.push('--viewEngine');
|
||||
}
|
||||
if (serverOptions.includeAutomaticOptionalChainCompletions) {
|
||||
argv.push('--includeAutomaticOptionalChainCompletions');
|
||||
}
|
||||
if (serverOptions.includeCompletionsWithSnippetText) {
|
||||
argv.push('--includeCompletionsWithSnippetText');
|
||||
}
|
||||
if (serverOptions.angularCoreVersion) {
|
||||
argv.push('--angularCoreVersion', serverOptions.angularCoreVersion);
|
||||
}
|
||||
const server = fork(SERVER_PATH, argv, {
|
||||
cwd: PROJECT_PATH,
|
||||
// uncomment to debug server process
|
||||
// execArgv: ['--inspect-brk=9330']
|
||||
});
|
||||
server.on('close', (code: number) => {
|
||||
if (code !== null && code !== 0) {
|
||||
throw new Error(`Server exited with code: ${code}`);
|
||||
}
|
||||
});
|
||||
const connection = createMessageConnection(
|
||||
new IPCMessageReader(server),
|
||||
new IPCMessageWriter(server),
|
||||
);
|
||||
connection.onDispose(() => {
|
||||
server.kill();
|
||||
});
|
||||
return connection;
|
||||
}
|
||||
|
||||
export function initializeServer(client: MessageConnection): Promise<lsp.InitializeResult> {
|
||||
return client.sendRequest(lsp.InitializeRequest.type, {
|
||||
/**
|
||||
* The process id of the parent process that started the server. It is
|
||||
* always the current process.
|
||||
*/
|
||||
processId: process.pid,
|
||||
rootUri: `file://${PROJECT_PATH}`,
|
||||
capabilities: {
|
||||
textDocument: {
|
||||
completion: {
|
||||
completionItem: {
|
||||
snippetSupport: true,
|
||||
},
|
||||
},
|
||||
moniker: {},
|
||||
definition: {linkSupport: true},
|
||||
typeDefinition: {linkSupport: true},
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Options are 'off' | 'messages' | 'verbose'.
|
||||
* To debug test failure, set to 'verbose'.
|
||||
*/
|
||||
trace: 'off',
|
||||
workspaceFolders: null,
|
||||
});
|
||||
}
|
||||
|
||||
export function openTextDocument(client: MessageConnection, filePath: string, newText?: string) {
|
||||
let languageId = 'unknown';
|
||||
if (filePath.endsWith('ts')) {
|
||||
languageId = 'typescript';
|
||||
} else if (filePath.endsWith('html')) {
|
||||
languageId = 'html';
|
||||
}
|
||||
client.sendNotification(lsp.DidOpenTextDocumentNotification.type, {
|
||||
textDocument: {
|
||||
uri: convertPathToFileUrl(filePath),
|
||||
languageId,
|
||||
version: 1,
|
||||
text: newText ?? fs.readFileSync(filePath, 'utf-8'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function convertPathToFileUrl(filePath: string): string {
|
||||
return URI.file(filePath).toString();
|
||||
}
|
||||
|
||||
export function createTracer(): lsp.Tracer {
|
||||
return {
|
||||
log(messageOrDataObject: string | any, data?: string) {
|
||||
if (typeof messageOrDataObject === 'string') {
|
||||
const message = messageOrDataObject;
|
||||
console.log(`[Trace - ${new Date().toLocaleTimeString()}] ${message}`);
|
||||
if (data) {
|
||||
console.log(data);
|
||||
}
|
||||
} else {
|
||||
const dataObject = messageOrDataObject;
|
||||
console.log(
|
||||
`[Trace - ${new Date().toLocaleTimeString()}] ` + JSON.stringify(dataObject, null, 2),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
load("@aspect_bazel_lib//lib:copy_to_bin.bzl", "copy_to_bin")
|
||||
load("@npm//:defs.bzl", "npm_link_all_packages")
|
||||
|
||||
npm_link_all_packages(name = "node_modules")
|
||||
|
||||
copy_to_bin(
|
||||
name = "pre_apf_project",
|
||||
srcs = glob(["**"]),
|
||||
visibility = [
|
||||
"//vscode-ng-language-service/server/src/tests:__pkg__",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
template: `<h1>Hello {{name}}</h1>`,
|
||||
})
|
||||
export class AppComponent {
|
||||
name = 'Angular';
|
||||
@Input() appInput = '';
|
||||
@Output() appOutput = new EventEmitter<string>();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
import {AppComponent} from './app.component';
|
||||
import {FooComponent} from './foo.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
declarations: [AppComponent, FooComponent],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{title | uppercase}}
|
||||
<span class="subtitle">
|
||||
subtitle
|
||||
</span>
|
||||
@@ -0,0 +1,8 @@
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
templateUrl: 'foo.component.html',
|
||||
})
|
||||
export class FooComponent {
|
||||
title = 'Foo Component';
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "angular-ls-integration-test-project",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "12.2.16",
|
||||
"@angular/compiler": "12.2.16",
|
||||
"@angular/compiler-cli": "12.2.16",
|
||||
"@angular/core": "12.2.16",
|
||||
"rxjs": "6.6.7",
|
||||
"zone.js": "0.11.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"target": "es2015",
|
||||
"strict": true,
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"strictTemplates": true,
|
||||
"strictInjectionParameters": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
load("@aspect_bazel_lib//lib:copy_to_bin.bzl", "copy_to_bin")
|
||||
load("@npm//:defs.bzl", "npm_link_all_packages")
|
||||
|
||||
npm_link_all_packages(name = "node_modules")
|
||||
|
||||
copy_to_bin(
|
||||
name = "pre_standalone_project",
|
||||
srcs = glob(["**"]),
|
||||
visibility = [
|
||||
"//vscode-ng-language-service/integration/lsp:__pkg__",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
template: `<h1>Hello {{name}}</h1>`,
|
||||
})
|
||||
export class AppComponent {
|
||||
name = 'Angular';
|
||||
@Input() appInput = '';
|
||||
@Output() appOutput = new EventEmitter<string>();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
import {AppComponent} from './app.component';
|
||||
import {FooComponent} from './foo.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
declarations: [AppComponent, FooComponent],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,4 @@
|
||||
{{title | uppercase}}
|
||||
<span class="subtitle">
|
||||
subtitle
|
||||
</span>
|
||||
@@ -0,0 +1,8 @@
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
templateUrl: 'foo.component.html',
|
||||
})
|
||||
export class FooComponent {
|
||||
title = 'Foo Component';
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "angular-ls-integration-test-project",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "18.2.10",
|
||||
"@angular/core": "18.2.10"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"target": "es2015",
|
||||
"strict": true,
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"strictTemplates": true,
|
||||
"strictInjectionParameters": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
load("@aspect_bazel_lib//lib:copy_to_bin.bzl", "copy_to_bin")
|
||||
load("@npm//:defs.bzl", "npm_link_all_packages")
|
||||
load("@npm//vscode-ng-language-service/integration/project:ng-packagr/package_json.bzl", ng_packagr_bin = "bin")
|
||||
|
||||
npm_link_all_packages(name = "node_modules")
|
||||
|
||||
copy_to_bin(
|
||||
name = "project_srcs",
|
||||
srcs = glob(["**"]),
|
||||
)
|
||||
|
||||
ng_packagr_bin.ng_packagr(
|
||||
name = "dist",
|
||||
srcs = [
|
||||
":node_modules",
|
||||
":project_srcs",
|
||||
],
|
||||
args = [
|
||||
"-p",
|
||||
"libs/post/ng-package.json",
|
||||
"-c",
|
||||
"libs/post/tsconfig.json",
|
||||
],
|
||||
chdir = package_name(),
|
||||
out_dirs = ["dist"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "project",
|
||||
srcs = [
|
||||
":dist",
|
||||
":project_srcs",
|
||||
],
|
||||
visibility = ["//vscode-ng-language-service/integration:__subpackages__"],
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
template: `<h1>Hello {{name}}</h1>`,
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent {
|
||||
name = 'Angular';
|
||||
@Input() appInput = '';
|
||||
@Output() appOutput = new EventEmitter<string>();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {NgModule} from '@angular/core';
|
||||
import {PostModule} from 'post';
|
||||
|
||||
import {AppComponent} from './app.component';
|
||||
import {FooComponent} from './foo.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, PostModule],
|
||||
declarations: [AppComponent, FooComponent],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,19 @@
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'baz-component',
|
||||
template: `<h1>Hello {{name}}</h1>`,
|
||||
standalone: true,
|
||||
})
|
||||
export class BazComponent {
|
||||
name = 'Angular';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'bar-component',
|
||||
template: `<`,
|
||||
standalone: true,
|
||||
})
|
||||
export class BarComponent {
|
||||
name = 'Angular';
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{{title | uppercase}}
|
||||
<span class="subtitle">
|
||||
subtitle
|
||||
{{sig()}}
|
||||
{{x.sig()}}
|
||||
</span>
|
||||
<lib-post></lib-post>
|
||||
@@ -0,0 +1,17 @@
|
||||
import {Component, signal} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
templateUrl: 'foo.component.html',
|
||||
standalone: false,
|
||||
})
|
||||
export class FooComponent {
|
||||
title = 'Foo Component';
|
||||
sig = signal(1);
|
||||
x = {
|
||||
sig: signal(1),
|
||||
};
|
||||
/** returns 1 */
|
||||
method() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/post",
|
||||
"lib": {
|
||||
"entryFile": "src/index.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "post",
|
||||
"version": "0.0.1",
|
||||
"peerDependencies": {
|
||||
"@angular/core": "^13.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './lib/post.component';
|
||||
@@ -0,0 +1,15 @@
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-post',
|
||||
template: '{{random}}',
|
||||
})
|
||||
export class PostComponent {
|
||||
random = Math.random();
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [PostComponent],
|
||||
exports: [PostComponent],
|
||||
})
|
||||
export class PostModule {}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"files": [],
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"outDir": "../../tsc-dist/post",
|
||||
"rootDir": ".",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"sourceMap": true,
|
||||
"types": []
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "angular-ls-integration-test-project",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "18.1.0-rc.0",
|
||||
"@angular/compiler": "18.1.0-rc.0",
|
||||
"@angular/compiler-cli": "18.1.0-rc.0",
|
||||
"@angular/core": "18.1.0-rc.0",
|
||||
"rxjs": "6.6.7",
|
||||
"zone.js": "0.11.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ng-packagr": "^18.0.0-next.0",
|
||||
"typescript": "~5.5.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "ng-packagr -p libs/post/ng-package.json -c libs/post/tsconfig.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"target": "es2015",
|
||||
"strict": true,
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"post": [
|
||||
"dist/post"
|
||||
]
|
||||
}
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"strictTemplates": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictInjectionParameters": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {join, resolve} from 'path';
|
||||
|
||||
import {convertPathToFileUrl} from './lsp/test_utils';
|
||||
|
||||
export const IS_BAZEL = !!process.env['TEST_TARGET'];
|
||||
export const PACKAGE_ROOT = IS_BAZEL ? resolve(__dirname, '..') : resolve(__dirname, '../..');
|
||||
export const SERVER_PATH = IS_BAZEL
|
||||
? join(PACKAGE_ROOT, 'server', 'index.js')
|
||||
: join(PACKAGE_ROOT, 'dist', 'npm', 'server', 'index.js');
|
||||
export const PROJECT_PATH = join(PACKAGE_ROOT, 'integration', 'project');
|
||||
export const PRE_STANDALONE_PROJECT_PATH = join(
|
||||
PACKAGE_ROOT,
|
||||
'integration',
|
||||
'pre_standalone_project',
|
||||
);
|
||||
|
||||
export const APP_COMPONENT = join(PROJECT_PATH, 'app', 'app.component.ts');
|
||||
export const APP_COMPONENT_URI = convertPathToFileUrl(APP_COMPONENT);
|
||||
export const BAR_COMPONENT = join(PROJECT_PATH, 'app', 'bar.component.ts');
|
||||
export const BAR_COMPONENT_URI = convertPathToFileUrl(BAR_COMPONENT);
|
||||
export const APP_COMPONENT_MODULE = join(PROJECT_PATH, 'app', 'app.module.ts');
|
||||
export const APP_COMPONENT_MODULE_URI = convertPathToFileUrl(APP_COMPONENT_MODULE);
|
||||
export const FOO_TEMPLATE = join(PROJECT_PATH, 'app', 'foo.component.html');
|
||||
export const FOO_TEMPLATE_URI = convertPathToFileUrl(FOO_TEMPLATE);
|
||||
export const FOO_COMPONENT = join(PROJECT_PATH, 'app', 'foo.component.ts');
|
||||
export const FOO_COMPONENT_URI = convertPathToFileUrl(FOO_COMPONENT);
|
||||
export const TSCONFIG = join(PROJECT_PATH, 'tsconfig.json');
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDirs": [
|
||||
"..",
|
||||
"dist"
|
||||
]
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"root":["./test_constants.ts","./e2e/completion_spec.ts","./e2e/definition_spec.ts","./e2e/helper.ts","./e2e/hover_spec.ts","./e2e/index.ts","./e2e/jasmine.ts","./lsp/ivy_spec.ts","./lsp/test_utils.ts","./pre_apf_project/app/app.component.ts","./pre_apf_project/app/app.module.ts","./pre_apf_project/app/foo.component.ts","./pre_standalone_project/app/app.component.ts","./pre_standalone_project/app/app.module.ts","./pre_standalone_project/app/foo.component.ts","./project/app/app.component.ts","./project/app/app.module.ts","./project/app/bar.component.ts","./project/app/foo.component.ts","./project/dist/post/index.d.ts","./project/dist/post/lib/post.component.d.ts","./project/libs/post/src/index.ts","./project/libs/post/src/lib/post.component.ts","./workspace/projects/demo/src/app/app.component.ts","./workspace/projects/demo/src/app/app.module.ts"],"errors":true,"version":"5.9.2"}
|
||||
@@ -0,0 +1,12 @@
|
||||
load("@aspect_bazel_lib//lib:copy_to_bin.bzl", "copy_to_bin")
|
||||
load("@npm//:defs.bzl", "npm_link_all_packages")
|
||||
|
||||
npm_link_all_packages(name = "node_modules")
|
||||
|
||||
copy_to_bin(
|
||||
name = "workspace",
|
||||
srcs = glob(["**"]),
|
||||
visibility = [
|
||||
"//vscode-ng-language-service/server/src/tests:__pkg__",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "workspace",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "13.3.9",
|
||||
"@angular/compiler": "13.3.9",
|
||||
"@angular/compiler-cli": "13.3.9",
|
||||
"@angular/core": "13.3.9",
|
||||
"rxjs": "6.6.7",
|
||||
"zone.js": "0.11.5"
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: `<h1>Welcome to {{title}}!</h1>`,
|
||||
styles: [],
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'demo';
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
import {AppComponent} from './app.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/app/app.module.ts",
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2015",
|
||||
"module": "es2020",
|
||||
"lib": [
|
||||
"es2018",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true,
|
||||
"typeCheckHostBindings": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
{
|
||||
"name": "ng-template",
|
||||
"displayName": "Angular Language Service",
|
||||
"description": "Editor services for Angular templates",
|
||||
"version": "20.2.2",
|
||||
"publisher": "Angular",
|
||||
"icon": "angular.png",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"Angular",
|
||||
"multi-root ready"
|
||||
],
|
||||
"engines": {
|
||||
"vscode": "^1.74.3"
|
||||
},
|
||||
"capabilities": {
|
||||
"untrustedWorkspaces": {
|
||||
"supported": true
|
||||
},
|
||||
"virtualWorkspaces": {
|
||||
"supported": "limited",
|
||||
"description": "The Language Server Protocol does not support remote file systems. Functionality is limited to syntax highlighting only."
|
||||
}
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
],
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "angular.restartNgServer",
|
||||
"title": "Restart Angular Language server",
|
||||
"category": "Angular"
|
||||
},
|
||||
{
|
||||
"command": "angular.openLogFile",
|
||||
"title": "Open Angular Server log",
|
||||
"category": "Angular"
|
||||
},
|
||||
{
|
||||
"command": "angular.getTemplateTcb",
|
||||
"title": "View Template Typecheck Block",
|
||||
"category": "Angular"
|
||||
},
|
||||
{
|
||||
"command": "angular.goToComponentWithTemplateFile",
|
||||
"title": "Go to component",
|
||||
"category": "Angular"
|
||||
},
|
||||
{
|
||||
"command": "angular.goToTemplateForComponent",
|
||||
"title": "Go to template",
|
||||
"category": "Angular"
|
||||
}
|
||||
],
|
||||
"submenus": [
|
||||
{
|
||||
"id": "angular.submenu",
|
||||
"label": "Angular"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "angular.goToComponentWithTemplateFile",
|
||||
"when": "editorLangId == html && !virtualWorkspace"
|
||||
},
|
||||
{
|
||||
"command": "angular.goToTemplateForComponent",
|
||||
"when": "editorLangId == typescript && !virtualWorkspace"
|
||||
},
|
||||
{
|
||||
"command": "angular.getTemplateTcb",
|
||||
"when": "!virtualWorkspace"
|
||||
}
|
||||
],
|
||||
"editor/context": [
|
||||
{
|
||||
"submenu": "angular.submenu",
|
||||
"group": "angular"
|
||||
}
|
||||
],
|
||||
"angular.submenu": [
|
||||
{
|
||||
"when": "(resourceLangId == html || resourceLangId == typescript) && !virtualWorkspace",
|
||||
"command": "angular.getTemplateTcb",
|
||||
"group": "debug"
|
||||
},
|
||||
{
|
||||
"when": "resourceLangId == html && !virtualWorkspace",
|
||||
"command": "angular.goToComponentWithTemplateFile",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"when": "resourceLangId == typescript && !virtualWorkspace",
|
||||
"command": "angular.goToTemplateForComponent",
|
||||
"group": "navigation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"title": "Angular Language Service",
|
||||
"properties": {
|
||||
"angular.log": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"off",
|
||||
"terse",
|
||||
"normal",
|
||||
"verbose"
|
||||
],
|
||||
"default": "off",
|
||||
"description": "Enables logging of the Angular server to a file. This log can be used to diagnose Angular Server issues. The log may contain file paths, source code, and other potentially sensitive information from your project."
|
||||
},
|
||||
"angular.enable-strict-mode-prompt": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"markdownDescription": "Prompt to enable the [strictTemplates](https://angular.dev/reference/configs/angular-compiler-options#stricttemplates) flag in [angularCompilerOptions](https://angular.dev/reference/configs/angular-compiler-options). Note that strict mode is only available when using Ivy."
|
||||
},
|
||||
"angular.trace.server": {
|
||||
"type": "string",
|
||||
"scope": "window",
|
||||
"enum": [
|
||||
"off",
|
||||
"messages",
|
||||
"verbose"
|
||||
],
|
||||
"default": "off",
|
||||
"description": "Traces the communication between VS Code and the Angular language server."
|
||||
},
|
||||
"angular.suggest.includeAutomaticOptionalChainCompletions": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"markdownDescription": "Enable showing completions on potentially undefined values that insert an optional chain call. Requires TS 3.7+ and strict null checks to be enabled."
|
||||
},
|
||||
"angular.suggest.includeCompletionsWithSnippetText": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"markdownDescription": "Enable snippet completions from Angular language server. Requires using TypeScript 4.3+ in the workspace."
|
||||
},
|
||||
"angular.suggest.autoImports": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"markdownDescription": "Enable/disable auto import suggestions for the exported Angular components from the current project."
|
||||
},
|
||||
"angular.forceStrictTemplates": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"markdownDescription": "Enabling this option will force the language service to use [strictTemplates](https://angular.dev/reference/configs/angular-compiler-options#stricttemplates) and ignore the user settings in the `tsconfig.json`."
|
||||
},
|
||||
"angular.suppressAngularDiagnosticCodes": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"markdownDescription": "A comma-separated list of error codes in templates whose diagnostics should be ignored."
|
||||
}
|
||||
}
|
||||
},
|
||||
"grammars": [
|
||||
{
|
||||
"path": "./syntaxes/inline-template.json",
|
||||
"scopeName": "inline-template.ng",
|
||||
"injectTo": [
|
||||
"source.ts"
|
||||
],
|
||||
"embeddedLanguages": {
|
||||
"text.html.derivative": "html",
|
||||
"source.css": "css",
|
||||
"source.js": "javascript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "./syntaxes/inline-styles.json",
|
||||
"scopeName": "inline-styles.ng",
|
||||
"injectTo": [
|
||||
"source.ts"
|
||||
],
|
||||
"embeddedLanguages": {
|
||||
"source.css.scss": "scss"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "./syntaxes/template.json",
|
||||
"scopeName": "template.ng",
|
||||
"injectTo": [
|
||||
"text.html.derivative",
|
||||
"source.ts"
|
||||
],
|
||||
"embeddedLanguages": {
|
||||
"text.html": "html",
|
||||
"source.css": "css",
|
||||
"expression.ng": "javascript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "./syntaxes/template-blocks.json",
|
||||
"scopeName": "template.blocks.ng",
|
||||
"injectTo": [
|
||||
"text.html.derivative",
|
||||
"source.ts"
|
||||
],
|
||||
"embeddedLanguages": {
|
||||
"text.html": "html",
|
||||
"control.block.expression.ng": "javascript",
|
||||
"control.block.body.ng": "html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "./syntaxes/let-declaration.json",
|
||||
"scopeName": "template.let.ng",
|
||||
"injectTo": [
|
||||
"text.html.derivative",
|
||||
"source.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "./syntaxes/host-object-literal.json",
|
||||
"scopeName": "host-object-literal.ng",
|
||||
"injectTo": [
|
||||
"source.ts"
|
||||
],
|
||||
"embeddedLanguages": {
|
||||
"text.html.derivative": "html",
|
||||
"expression.ng": "javascript",
|
||||
"source.ts": "typescript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "./syntaxes/template-tag.json",
|
||||
"scopeName": "template.tag.ng",
|
||||
"injectTo": [
|
||||
"text.html.derivative",
|
||||
"source.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "./syntaxes/expression.json",
|
||||
"scopeName": "expression.ng"
|
||||
}
|
||||
]
|
||||
},
|
||||
"activationEvents": [
|
||||
"onLanguage:html",
|
||||
"onLanguage:typescript"
|
||||
],
|
||||
"main": "./bazel-bin/client/src/extension",
|
||||
"scripts": {
|
||||
"ng-dev": "tsx --tsconfig .ng-dev/tsconfig.json node_modules/@angular/ng-dev/bundles/cli.mjs",
|
||||
"build:syntaxes": "bazel run //vscode-ng-language-service/syntaxes:syntaxes",
|
||||
"format": "pnpm ng-dev format all",
|
||||
"watch": "ibazel build //vscode-ng-language-service:index.js //vscode-ng-language-service/server:index.js",
|
||||
"package": "pnpm bazel build //vscode-ng-language-service:npm --config=release",
|
||||
"test": "bazel test --test_tag_filters=unit_test //vscode-ng-language-service/...",
|
||||
"test:watch": "ibazel test --test_tag_filters=unit_test //vscode-ng-language-service/...",
|
||||
"test:lsp": "bazel test --test_output=streamed //vscode-ng-language-service/integration/lsp:test",
|
||||
"test:e2e": "bazel test --test_output=streamed //vscode-ng-language-service/integration/e2e:test",
|
||||
"test:inspect-client": "bazel run --config=debug //vscode-ng-language-service/client/src/tests:test",
|
||||
"test:inspect-common": "bazel run --config=debug //vscode-ng-language-service/common/tests:test",
|
||||
"test:inspect-server": "bazel run --config=debug //vscode-ng-language-service/server/src/tests:test",
|
||||
"test:inspect-syntaxes": "bazel run --config=debug //vscode-ng-language-service/syntaxes/test:test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/language-service": "20.3.0",
|
||||
"typescript": "5.9.2",
|
||||
"vscode-html-languageservice": "^4.2.5",
|
||||
"vscode-jsonrpc": "6.0.0",
|
||||
"vscode-languageclient": "7.0.0",
|
||||
"vscode-languageserver": "7.0.0",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
"vscode-uri": "3.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/core": "^20.0.0-rc.2",
|
||||
"@types/jasmine": "3.10.7",
|
||||
"@types/node": "18.19.39",
|
||||
"@types/vscode": "1.67.0",
|
||||
"balanced-match": "1.0.2",
|
||||
"brace-expansion": "1.1.12",
|
||||
"concat-map": "0.0.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "0.14.39",
|
||||
"jasmine-core": "5.10.0",
|
||||
"jasmine-reporters": "2.5.2",
|
||||
"jasmine": "5.10.0",
|
||||
"lru-cache": "6.0.0",
|
||||
"minimatch": "3.1.2",
|
||||
"prettier": "2.7.1",
|
||||
"rxjs": "6.6.7",
|
||||
"semver": "7.7.2",
|
||||
"source-map-support": "0.5.21",
|
||||
"ts-node": "^10.8.1",
|
||||
"vsce": "1.100.1",
|
||||
"vscode-languageserver-protocol": "3.16.0",
|
||||
"vscode-languageserver-types": "3.16.0",
|
||||
"vscode-nls": "5.2.0",
|
||||
"vscode-test": "1.6.1",
|
||||
"vscode-tmgrammar-test": "0.0.11",
|
||||
"yallist": "4.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/angular/vscode-ng-language-service"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
load("@aspect_bazel_lib//lib:expand_template.bzl", "expand_template_rule")
|
||||
load("@aspect_rules_esbuild//esbuild:defs.bzl", "esbuild")
|
||||
load("@aspect_rules_ts//ts:defs.bzl", "ts_config")
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
visibility = [
|
||||
"//vscode-ng-language-service/integration:__subpackages__",
|
||||
"//vscode-ng-language-service/server:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//vscode-ng-language-service:tsconfig",
|
||||
],
|
||||
)
|
||||
|
||||
esbuild(
|
||||
name = "banner",
|
||||
config = {
|
||||
# This is described in more detail in the `server/banner.ts` but this line actually overrides
|
||||
# the built-in `require` function by adding a line at the bottom of the generated banner code
|
||||
# to assign the override function to the `require` name.
|
||||
"footer": {"js": "require = requireOverride;"},
|
||||
# Workaround for https://github.com/aspect-build/rules_esbuild/issues/58
|
||||
"resolveExtensions": [".js"],
|
||||
},
|
||||
entry_point = "//vscode-ng-language-service/server/src:banner.js",
|
||||
external = [
|
||||
"path",
|
||||
],
|
||||
format = "cjs",
|
||||
# Do not enable minification. It seems to break the extension on Windows (with WSL). See #1198.
|
||||
minify = False,
|
||||
platform = "node",
|
||||
sourcemap = "external",
|
||||
deps = [
|
||||
"//:node_modules/source-map-support",
|
||||
"//vscode-ng-language-service/server/src",
|
||||
],
|
||||
)
|
||||
|
||||
esbuild(
|
||||
name = "index",
|
||||
srcs = [
|
||||
":banner",
|
||||
],
|
||||
config = "esbuild.mjs",
|
||||
entry_point = "//vscode-ng-language-service/server/src:server.js",
|
||||
external = [
|
||||
"fs",
|
||||
"path",
|
||||
"typescript/lib/tsserverlibrary",
|
||||
"vscode-languageserver",
|
||||
"vscode-uri",
|
||||
"vscode-jsonrpc",
|
||||
"vscode-languageserver-textdocument",
|
||||
"vscode-html-languageservice",
|
||||
],
|
||||
format = "cjs",
|
||||
# Do not enable minification. It seems to break the extension on Windows (with WSL). See #1198.
|
||||
minify = False,
|
||||
platform = "node",
|
||||
sourcemap = "external",
|
||||
visibility = [
|
||||
"//vscode-ng-language-service/integration:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//:node_modules/source-map-support",
|
||||
"//vscode-ng-language-service/server/src",
|
||||
],
|
||||
)
|
||||
|
||||
expand_template_rule(
|
||||
name = "package_json_expanded",
|
||||
out = "package_expanded.json",
|
||||
stamp_substitutions = {
|
||||
"0.0.0-PLACEHOLDER": "{{BUILD_SCM_VERSION}}",
|
||||
},
|
||||
template = "package.json",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "npm_files",
|
||||
srcs = [
|
||||
"README.md",
|
||||
"bin/ngserver",
|
||||
"index.js",
|
||||
"package_expanded.json",
|
||||
],
|
||||
visibility = ["//vscode-ng-language-service:__pkg__"],
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
# @angular/language-server
|
||||
|
||||
This package contains the Language Server Protocol ([LSP](https://microsoft.github.io/language-server-protocol/)) implementation of the Angular Language Service.
|
||||
|
||||
## Usage
|
||||
See `node index.js --help`
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
require('../index.js')
|
||||
@@ -0,0 +1,7 @@
|
||||
import {readFileSync} from 'fs';
|
||||
const banner = readFileSync('vscode-ng-language-service/server/banner.js', 'utf8');
|
||||
export default {
|
||||
banner: {js: banner},
|
||||
// Workaround for https://github.com/aspect-build/rules_esbuild/issues/58
|
||||
resolveExtensions: ['.js'],
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@angular/language-server",
|
||||
"description": "LSP server for Angular Language Service",
|
||||
"version": "0.0.0-PLACEHOLDER",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/angular/vscode-ng-language-service.git"
|
||||
},
|
||||
"author": "Angular",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.11.1 || ^22.11.0"
|
||||
},
|
||||
"bin": {
|
||||
"ngserver": "./bin/ngserver"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/language-service": "20.2.0-rc.1",
|
||||
"vscode-html-languageservice": "^4.2.5",
|
||||
"vscode-jsonrpc": "6.0.0",
|
||||
"vscode-languageserver": "7.0.0",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
"vscode-uri": "3.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "3.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://wombat-dressing-room.appspot.com"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
|
||||
|
||||
ts_project(
|
||||
name = "src",
|
||||
srcs = glob(["*.ts"]),
|
||||
declaration = True,
|
||||
source_map = True,
|
||||
tags = [
|
||||
# Because the testing intentionally reaches into the output directories and crosses the sandbox
|
||||
# boundaries, we need to explicitly make sure that this target is built locally so the testing
|
||||
# can find expected files.
|
||||
"no-remote-exec",
|
||||
],
|
||||
tsconfig = "//vscode-ng-language-service/server:tsconfig",
|
||||
visibility = [
|
||||
"//vscode-ng-language-service/server:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//:node_modules/@angular/language-service",
|
||||
"//:node_modules/typescript",
|
||||
"//vscode-ng-language-service:node_modules/@types/node",
|
||||
"//vscode-ng-language-service:node_modules/vscode-html-languageservice",
|
||||
"//vscode-ng-language-service:node_modules/vscode-languageserver",
|
||||
"//vscode-ng-language-service:node_modules/vscode-languageserver-textdocument",
|
||||
"//vscode-ng-language-service:node_modules/vscode-uri",
|
||||
"//vscode-ng-language-service/common",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {parseCommandLine} from './cmdline_utils';
|
||||
import {resolveTsServer} from './version_provider';
|
||||
|
||||
const originalRequire = require;
|
||||
|
||||
/**
|
||||
* This method provides a custom implementation for the require function to resolve
|
||||
* `typescript` module at runtime. We provide this as an override to the built-in
|
||||
* 'require' method using the banner funcionality of esbuild. That is, esbuild will
|
||||
* compile the server and add this banner to the top of the compilation so any place
|
||||
* in the server code that uses `require` will get routed through this override.
|
||||
*
|
||||
* Refer also to `esbuild` rules in the server package, the `bannerConfig` which overrides the
|
||||
* `require` using the `footer` option, and the `serverConfig` which provides the banner code at the
|
||||
* top of the server output using the `banner` option.
|
||||
*
|
||||
* @param moduleName The module to resolve
|
||||
* @returns
|
||||
*/
|
||||
export function requireOverride(moduleName: string) {
|
||||
const TSSERVER = 'typescript/lib/tsserverlibrary';
|
||||
if (moduleName === 'typescript') {
|
||||
throw new Error(`Import '${TSSERVER}' instead of 'typescript'`);
|
||||
}
|
||||
if (moduleName === TSSERVER) {
|
||||
const {tsProbeLocations, tsdk} = parseCommandLine(process.argv);
|
||||
moduleName = resolveTsServer(tsProbeLocations, tsdk).resolvedPath;
|
||||
}
|
||||
return originalRequire(moduleName);
|
||||
}
|
||||
|
||||
requireOverride.resolve = originalRequire.resolve;
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
function findArgument(argv: string[], argName: string): string | undefined {
|
||||
const index = argv.indexOf(argName);
|
||||
if (index < 0 || index === argv.length - 1) {
|
||||
return;
|
||||
}
|
||||
return argv[index + 1];
|
||||
}
|
||||
|
||||
function findArgumentWithDefault<T>(
|
||||
argv: string[],
|
||||
argName: string,
|
||||
defaultValue: T,
|
||||
): T | string | undefined {
|
||||
const index = argv.indexOf(argName);
|
||||
if (index < 0) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (index === argv.length - 1) {
|
||||
return undefined;
|
||||
}
|
||||
const argValue = argv[index + 1];
|
||||
if (argValue.startsWith('-')) {
|
||||
return undefined;
|
||||
} else {
|
||||
return argValue;
|
||||
}
|
||||
}
|
||||
|
||||
function parseBooleanArgument(argv: string[], argName: string): boolean {
|
||||
const argValue = findArgumentWithDefault(argv, argName, 'true');
|
||||
if (argValue === undefined || argValue === 'true') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function parseStringArray(argv: string[], argName: string): string[] {
|
||||
const arg = findArgument(argv, argName);
|
||||
if (!arg) {
|
||||
return [];
|
||||
}
|
||||
return arg.split(',');
|
||||
}
|
||||
|
||||
function hasArgument(argv: string[], argName: string): boolean {
|
||||
return argv.includes(argName);
|
||||
}
|
||||
|
||||
interface CommandLineOptions {
|
||||
help: boolean;
|
||||
logFile?: string;
|
||||
logVerbosity?: string;
|
||||
logToConsole: boolean;
|
||||
ngProbeLocations: string[];
|
||||
tsProbeLocations: string[];
|
||||
tsdk: string | null;
|
||||
includeAutomaticOptionalChainCompletions: boolean;
|
||||
includeCompletionsWithSnippetText: boolean;
|
||||
includeCompletionsForModuleExports: boolean;
|
||||
forceStrictTemplates: boolean;
|
||||
disableBlockSyntax: boolean;
|
||||
disableLetSyntax: boolean;
|
||||
angularCoreVersion?: string;
|
||||
suppressAngularDiagnosticCodes?: string;
|
||||
}
|
||||
|
||||
export function parseCommandLine(argv: string[]): CommandLineOptions {
|
||||
return {
|
||||
help: hasArgument(argv, '--help'),
|
||||
logFile: findArgument(argv, '--logFile'),
|
||||
logVerbosity: findArgument(argv, '--logVerbosity'),
|
||||
logToConsole: hasArgument(argv, '--logToConsole'),
|
||||
ngProbeLocations: parseStringArray(argv, '--ngProbeLocations'),
|
||||
tsProbeLocations: parseStringArray(argv, '--tsProbeLocations'),
|
||||
tsdk: findArgument(argv, '--tsdk') ?? null,
|
||||
includeAutomaticOptionalChainCompletions: hasArgument(
|
||||
argv,
|
||||
'--includeAutomaticOptionalChainCompletions',
|
||||
),
|
||||
includeCompletionsWithSnippetText: hasArgument(argv, '--includeCompletionsWithSnippetText'),
|
||||
includeCompletionsForModuleExports: parseBooleanArgument(
|
||||
argv,
|
||||
'--includeCompletionsForModuleExports',
|
||||
),
|
||||
forceStrictTemplates: hasArgument(argv, '--forceStrictTemplates'),
|
||||
disableBlockSyntax: hasArgument(argv, '--disableBlockSyntax'),
|
||||
disableLetSyntax: hasArgument(argv, '--disableLetSyntax'),
|
||||
angularCoreVersion: findArgument(argv, '--angularCoreVersion'),
|
||||
suppressAngularDiagnosticCodes: findArgument(argv, '--suppressAngularDiagnosticCodes'),
|
||||
};
|
||||
}
|
||||
|
||||
export function generateHelpMessage(argv: string[]) {
|
||||
return `Angular Language Service that implements the Language Server Protocol (LSP).
|
||||
|
||||
Usage: ${argv[0]} ${argv[1]} [options]
|
||||
|
||||
Options:
|
||||
--help: Prints help message.
|
||||
--logFile: Location to log messages. Logging to file is disabled if not provided.
|
||||
--logVerbosity: terse|normal|verbose|requestTime. See ts.server.LogLevel.
|
||||
--logToConsole: Enables logging to console via 'window/logMessage'. Defaults to false.
|
||||
--ngProbeLocations: Path of @angular/language-service. Required.
|
||||
--tsProbeLocations: Path of typescript. Required.
|
||||
--includeAutomaticOptionalChainCompletions: Shows completions on potentially undefined values that insert an optional chain call. Requires TS 3.7+ and strict null checks to be enabled.
|
||||
--includeCompletionsWithSnippetText: Enables snippet completions from Angular language server;
|
||||
--forceStrictTemplates: Forces the language service to use strictTemplates and ignore the user settings in the 'tsconfig.json'.
|
||||
--suppressAngularDiagnosticCodes: A comma-separated list of error codes in templates whose diagnostics should be ignored.
|
||||
|
||||
Additional options supported by vscode-languageserver:
|
||||
--clientProcessId=<number>: Automatically kills the server if the client process dies.
|
||||
--node-ipc: Communicate using Node's IPC. This is the default.
|
||||
--stdio: Communicate over stdin/stdout.
|
||||
--socket=<number>: Communicate using Unix socket.
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||
import * as lsp from 'vscode-languageserver';
|
||||
|
||||
import {lspPositionToTsPosition, tsTextSpanToLspRange} from './utils';
|
||||
|
||||
// TODO: Move this to `@angular/language-service`.
|
||||
enum CompletionKind {
|
||||
attribute = 'attribute',
|
||||
block = 'block',
|
||||
htmlAttribute = 'html attribute',
|
||||
property = 'property',
|
||||
component = 'component',
|
||||
directive = 'directive',
|
||||
element = 'element',
|
||||
event = 'event',
|
||||
key = 'key',
|
||||
method = 'method',
|
||||
pipe = 'pipe',
|
||||
type = 'type',
|
||||
reference = 'reference',
|
||||
variable = 'variable',
|
||||
entity = 'entity',
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the origin of an `lsp.CompletionItem`, which is stored in the
|
||||
* `lsp.CompletionItem.data` property.
|
||||
*
|
||||
* On future requests for details about a completion item, this information allows the language
|
||||
* service to determine the context for the original completion request, in order to return more
|
||||
* detailed results.
|
||||
*/
|
||||
export interface NgCompletionOriginData {
|
||||
/**
|
||||
* Used to validate the type of `lsp.CompletionItem.data` is correct, since that field is type
|
||||
* `any`.
|
||||
*/
|
||||
kind: 'ngCompletionOriginData';
|
||||
|
||||
filePath: string;
|
||||
position: lsp.Position;
|
||||
|
||||
tsData?: ts.CompletionEntryData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract `NgCompletionOriginData` from an `lsp.CompletionItem` if present.
|
||||
*/
|
||||
export function readNgCompletionData(item: lsp.CompletionItem): NgCompletionOriginData | null {
|
||||
if (item.data === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate that `item.data.kind` is actually the right tag, and narrow its type in the process.
|
||||
const data: NgCompletionOriginData | {kind?: never} = item.data;
|
||||
if (data.kind !== 'ngCompletionOriginData') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Angular's CompletionKind to LSP CompletionItemKind.
|
||||
* @param kind Angular's CompletionKind
|
||||
*/
|
||||
function ngCompletionKindToLspCompletionItemKind(kind: CompletionKind): lsp.CompletionItemKind {
|
||||
switch (kind) {
|
||||
case CompletionKind.attribute:
|
||||
case CompletionKind.htmlAttribute:
|
||||
case CompletionKind.property:
|
||||
case CompletionKind.event:
|
||||
return lsp.CompletionItemKind.Property;
|
||||
case CompletionKind.directive:
|
||||
case CompletionKind.component:
|
||||
case CompletionKind.element:
|
||||
case CompletionKind.key:
|
||||
return lsp.CompletionItemKind.Class;
|
||||
case CompletionKind.method:
|
||||
return lsp.CompletionItemKind.Method;
|
||||
case CompletionKind.pipe:
|
||||
return lsp.CompletionItemKind.Function;
|
||||
case CompletionKind.type:
|
||||
return lsp.CompletionItemKind.Interface;
|
||||
case CompletionKind.reference:
|
||||
case CompletionKind.variable:
|
||||
return lsp.CompletionItemKind.Variable;
|
||||
case CompletionKind.block:
|
||||
return lsp.CompletionItemKind.Keyword;
|
||||
case CompletionKind.entity:
|
||||
default:
|
||||
return lsp.CompletionItemKind.Text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert ts.CompletionEntry to LSP Completion Item.
|
||||
* @param entry completion entry
|
||||
* @param position position where completion is requested.
|
||||
* @param scriptInfo
|
||||
*/
|
||||
export function tsCompletionEntryToLspCompletionItem(
|
||||
entry: ts.CompletionEntry,
|
||||
position: lsp.Position,
|
||||
scriptInfo: ts.server.ScriptInfo,
|
||||
): lsp.CompletionItem {
|
||||
const item = lsp.CompletionItem.create(entry.name);
|
||||
// Even though `entry.kind` is typed as ts.ScriptElementKind, it's
|
||||
// really Angular's CompletionKind. This is because ts.ScriptElementKind does
|
||||
// not sufficiently capture the HTML entities.
|
||||
// This is a limitation of being a tsserver plugin.
|
||||
const kind = entry.kind as unknown as CompletionKind;
|
||||
item.kind = ngCompletionKindToLspCompletionItemKind(kind);
|
||||
item.detail = entry.kind;
|
||||
item.sortText = entry.sortText;
|
||||
// Text that actually gets inserted to the document. It could be different
|
||||
// from 'entry.name'. For example, a method name could be 'greet', but the
|
||||
// insertText is 'greet()'.
|
||||
const insertText = entry.insertText || entry.name;
|
||||
item.textEdit = createTextEdit(scriptInfo, entry, position, insertText);
|
||||
|
||||
// If the user enables the config `includeAutomaticOptionalChainCompletions`, the `insertText`
|
||||
// range will include the dot. the `insertText` should be assigned to the `filterText` to filter
|
||||
// the completion items.
|
||||
item.filterText = entry.insertText;
|
||||
if (entry.isSnippet) {
|
||||
item.insertTextFormat = lsp.InsertTextFormat.Snippet;
|
||||
}
|
||||
|
||||
item.data = {
|
||||
kind: 'ngCompletionOriginData',
|
||||
filePath: scriptInfo.fileName,
|
||||
position,
|
||||
tsData: entry.data,
|
||||
} as NgCompletionOriginData;
|
||||
return item;
|
||||
}
|
||||
|
||||
function createTextEdit(
|
||||
scriptInfo: ts.server.ScriptInfo,
|
||||
entry: ts.CompletionEntry,
|
||||
position: lsp.Position,
|
||||
insertText: string,
|
||||
) {
|
||||
if (entry.replacementSpan === undefined) {
|
||||
return lsp.TextEdit.insert(position, insertText);
|
||||
} else {
|
||||
/**
|
||||
* The Angular Language Service does not return `InsertReplaceEdit`.
|
||||
* There is no need to allow the developer to choose how to insert the completion.
|
||||
*
|
||||
* For example, `<button (c|) />`.
|
||||
* ^^__________Insert edit
|
||||
* ^^ ^________Replace edit
|
||||
*
|
||||
* If the LS returns the `InsertReplaceEdit` as shown above, selecting "Insert" by the developer
|
||||
* results in `(click)="")`, and selecting "Replace" results in `(click)=""`.
|
||||
*
|
||||
* Now in the vscode, the default `editor.suggest.insertMode` value for HTML is `Replace`, for
|
||||
* ts is `Insert`, So this leads to a bug in the ts file.
|
||||
*
|
||||
* Fixes https://github.com/angular/vscode-ng-language-service/issues/2137
|
||||
*/
|
||||
return lsp.TextEdit.replace(
|
||||
tsTextSpanToLspRange(scriptInfo, entry.replacementSpan),
|
||||
insertText,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||
import * as lsp from 'vscode-languageserver';
|
||||
|
||||
import {tsRelatedInformationToLspRelatedInformation, tsTextSpanToLspRange} from './utils';
|
||||
|
||||
/**
|
||||
* Convert ts.DiagnosticCategory to lsp.DiagnosticSeverity
|
||||
* @param category diagnostic category
|
||||
*/
|
||||
function tsDiagnosticCategoryToLspDiagnosticSeverity(category: ts.DiagnosticCategory) {
|
||||
switch (category) {
|
||||
case ts.DiagnosticCategory.Warning:
|
||||
return lsp.DiagnosticSeverity.Warning;
|
||||
case ts.DiagnosticCategory.Error:
|
||||
return lsp.DiagnosticSeverity.Error;
|
||||
case ts.DiagnosticCategory.Suggestion:
|
||||
return lsp.DiagnosticSeverity.Hint;
|
||||
case ts.DiagnosticCategory.Message:
|
||||
default:
|
||||
return lsp.DiagnosticSeverity.Information;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert ts.Diagnostic to lsp.Diagnostic
|
||||
* @param tsDiag TS diagnostic
|
||||
* @param scriptInfo Used to compute proper offset.
|
||||
*/
|
||||
export function tsDiagnosticToLspDiagnostic(
|
||||
tsDiag: ts.Diagnostic,
|
||||
projectService: ts.server.ProjectService,
|
||||
): lsp.Diagnostic {
|
||||
const textSpan: ts.TextSpan = {
|
||||
start: tsDiag.start || 0,
|
||||
length: tsDiag.length || 0,
|
||||
};
|
||||
|
||||
const diagScriptInfo =
|
||||
tsDiag.file !== undefined ? projectService.getScriptInfo(tsDiag.file.fileName) : undefined;
|
||||
const range =
|
||||
diagScriptInfo !== undefined
|
||||
? tsTextSpanToLspRange(diagScriptInfo, textSpan)
|
||||
: lsp.Range.create(0, 0, 0, 0);
|
||||
const diag = lsp.Diagnostic.create(
|
||||
range,
|
||||
ts.flattenDiagnosticMessageText(tsDiag.messageText, '\n'),
|
||||
tsDiagnosticCategoryToLspDiagnosticSeverity(tsDiag.category),
|
||||
tsDiag.code,
|
||||
tsDiag.source,
|
||||
tsRelatedInformationToLspRelatedInformation(projectService, tsDiag.relatedInformation),
|
||||
);
|
||||
diag.tags = tsDiag.reportsDeprecated !== undefined ? [lsp.DiagnosticTag.Deprecated] : undefined;
|
||||
|
||||
return diag;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
/**
|
||||
* Takes a TS file and strips out all non-inline template content.
|
||||
*
|
||||
* This process is the same as what's done in the VSCode example for embedded languages.
|
||||
*
|
||||
* Note that the example below implements the support on the client side. This is done on the server
|
||||
* side to enable language services in other editors to take advantage of this feature by depending
|
||||
* on the @angular/language-server package.
|
||||
*
|
||||
* @see https://github.com/microsoft/vscode-extension-samples/blob/fdd3bb95ce8e38ffe58fc9158797239fdf5017f1/lsp-embedded-request-forwarding/client/src/embeddedSupport.ts#L131-L141
|
||||
* @see https://code.visualstudio.com/api/language-extensions/embedded-languages
|
||||
*/
|
||||
export function getHTMLVirtualContent(sf: ts.SourceFile): string {
|
||||
const inlineTemplateNodes: ts.Node[] = findAllMatchingNodes(sf, isInlineTemplateNode);
|
||||
const documentText = sf.text;
|
||||
|
||||
// Create a blank document with same text length
|
||||
let content = documentText
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
return ' '.repeat(line.length);
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
// add back all the inline template regions in-place
|
||||
for (const region of inlineTemplateNodes) {
|
||||
content =
|
||||
content.slice(0, region.getStart(sf) + 1) +
|
||||
documentText.slice(region.getStart(sf) + 1, region.getEnd() - 1) +
|
||||
content.slice(region.getEnd() - 1);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function isInlineTemplateNode(node: ts.Node) {
|
||||
const assignment = getPropertyAssignmentFromValue(node, 'template');
|
||||
return (
|
||||
ts.isStringLiteralLike(node) &&
|
||||
assignment !== null &&
|
||||
getClassDeclFromDecoratorProp(assignment) !== null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a property assignment from the assignment value if the property name
|
||||
* matches the specified `key`, or `null` if there is no match.
|
||||
*/
|
||||
export function getPropertyAssignmentFromValue(
|
||||
value: ts.Node,
|
||||
key: string,
|
||||
): ts.PropertyAssignment | null {
|
||||
const propAssignment = value.parent;
|
||||
if (
|
||||
!propAssignment ||
|
||||
!ts.isPropertyAssignment(propAssignment) ||
|
||||
propAssignment.name.getText() !== key
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return propAssignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a decorator property assignment, return the ClassDeclaration node that corresponds to the
|
||||
* directive class the property applies to.
|
||||
* If the property assignment is not on a class decorator, no declaration is returned.
|
||||
*
|
||||
* For example,
|
||||
*
|
||||
* @Component({
|
||||
* template: '<div></div>'
|
||||
* ^^^^^^^^^^^^^^^^^^^^^^^---- property assignment
|
||||
* })
|
||||
* class AppComponent {}
|
||||
* ^---- class declaration node
|
||||
*
|
||||
* @param propAsgnNode property assignment
|
||||
*/
|
||||
export function getClassDeclFromDecoratorProp(
|
||||
propAsgnNode: ts.PropertyAssignment,
|
||||
): ts.ClassDeclaration | undefined {
|
||||
if (!propAsgnNode.parent || !ts.isObjectLiteralExpression(propAsgnNode.parent)) {
|
||||
return;
|
||||
}
|
||||
const objLitExprNode = propAsgnNode.parent;
|
||||
if (!objLitExprNode.parent || !ts.isCallExpression(objLitExprNode.parent)) {
|
||||
return;
|
||||
}
|
||||
const callExprNode = objLitExprNode.parent;
|
||||
if (!callExprNode.parent || !ts.isDecorator(callExprNode.parent)) {
|
||||
return;
|
||||
}
|
||||
const decorator = callExprNode.parent;
|
||||
if (!decorator.parent || !ts.isClassDeclaration(decorator.parent)) {
|
||||
return;
|
||||
}
|
||||
const classDeclNode = decorator.parent;
|
||||
return classDeclNode;
|
||||
}
|
||||
|
||||
export function findAllMatchingNodes(
|
||||
sf: ts.SourceFile,
|
||||
filter: (node: ts.Node) => boolean,
|
||||
): ts.Node[] {
|
||||
const results: ts.Node[] = [];
|
||||
const stack: ts.Node[] = [sf];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
|
||||
if (filter(node)) {
|
||||
results.push(node);
|
||||
} else {
|
||||
stack.push(...node.getChildren());
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||
|
||||
// NOTES:
|
||||
// Be very careful about logging. There are two types of logging:
|
||||
// 1. Console
|
||||
// 2. File
|
||||
// The language server could operate in a few different modes, depending on
|
||||
// startup options. `process.argv` is parsed by `vscode-languageserver` when
|
||||
// createConnection() is called.
|
||||
// By default, the server communicates with the client through Node IPC.
|
||||
// In this case, logging to stdout/stderr would be piped to VSCode's
|
||||
// development console (Output tab in vscode).
|
||||
// Verbose log should not be sent to the client as it could negatively impact
|
||||
// performance. Instead, verbose log entries should be written to file using
|
||||
// the Logger class here.
|
||||
// The language server could also operate in JSON-RPC mode via stdin/stdout.
|
||||
// In this case, there must not be any logging done through console.log(),
|
||||
// console.info() etc, as it could pollute the communication channel.
|
||||
// TLDR: To log to development console, always use connection.console.log().
|
||||
// Never use console.log(), console.info(), etc directly.
|
||||
|
||||
export interface LoggerOptions {
|
||||
logFile?: string;
|
||||
logVerbosity?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a logger instance to write to file.
|
||||
* @param options Logging options.
|
||||
*/
|
||||
export function createLogger(options: LoggerOptions): ts.server.Logger {
|
||||
let logLevel: ts.server.LogLevel;
|
||||
switch (options.logVerbosity) {
|
||||
case 'requestTime':
|
||||
logLevel = ts.server.LogLevel.requestTime;
|
||||
break;
|
||||
case 'verbose':
|
||||
logLevel = ts.server.LogLevel.verbose;
|
||||
break;
|
||||
case 'normal':
|
||||
logLevel = ts.server.LogLevel.normal;
|
||||
break;
|
||||
case 'terse':
|
||||
default:
|
||||
logLevel = ts.server.LogLevel.terse;
|
||||
break;
|
||||
}
|
||||
return new Logger(logLevel, options.logFile);
|
||||
}
|
||||
|
||||
// TODO: Code below is from TypeScript's repository. Maybe create our own
|
||||
// implementation.
|
||||
// https://github.com/microsoft/TypeScript/blob/ec39d412876d0dcf704fc886d5036cb625220d2f/src/tsserver/server.ts#L120
|
||||
|
||||
function noop(_?: {} | null | undefined): void {} // tslint:disable-line no-empty
|
||||
|
||||
function nowString() {
|
||||
// E.g. "12:34:56.789"
|
||||
const d = new Date();
|
||||
return `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}.${d.getMilliseconds()}`;
|
||||
}
|
||||
|
||||
class Logger implements ts.server.Logger {
|
||||
private readonly fd: number;
|
||||
private seq = 0;
|
||||
private inGroup = false;
|
||||
private firstInGroup = true;
|
||||
|
||||
constructor(
|
||||
private readonly level: ts.server.LogLevel,
|
||||
private readonly logFilename?: string,
|
||||
) {
|
||||
this.fd = -1;
|
||||
if (logFilename) {
|
||||
try {
|
||||
const dir = path.dirname(logFilename);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir);
|
||||
}
|
||||
this.fd = fs.openSync(logFilename, 'w');
|
||||
} catch {
|
||||
// swallow the error and keep logging disabled if file cannot be opened
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.loggingEnabled()) {
|
||||
fs.close(this.fd, noop);
|
||||
}
|
||||
}
|
||||
|
||||
getLogFileName() {
|
||||
return this.logFilename;
|
||||
}
|
||||
|
||||
perftrc(s: string) {
|
||||
this.msg(s, ts.server.Msg.Perf);
|
||||
}
|
||||
|
||||
info(s: string) {
|
||||
this.msg(s, ts.server.Msg.Info);
|
||||
}
|
||||
|
||||
startGroup() {
|
||||
this.inGroup = true;
|
||||
this.firstInGroup = true;
|
||||
}
|
||||
|
||||
endGroup() {
|
||||
this.inGroup = false;
|
||||
}
|
||||
|
||||
loggingEnabled() {
|
||||
return this.fd >= 0;
|
||||
}
|
||||
|
||||
hasLevel(level: ts.server.LogLevel) {
|
||||
return this.loggingEnabled() && this.level >= level;
|
||||
}
|
||||
|
||||
msg(s: string, type: ts.server.Msg = ts.server.Msg.Err) {
|
||||
if (!this.loggingEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let prefix = '';
|
||||
if (!this.inGroup || this.firstInGroup) {
|
||||
this.firstInGroup = false;
|
||||
prefix = `${type} ${this.seq}`.padEnd(10) + `[${nowString()}] `;
|
||||
}
|
||||
const entry = prefix + s + '\n';
|
||||
|
||||
fs.writeSync(this.fd, entry);
|
||||
|
||||
if (!this.inGroup) {
|
||||
this.seq++;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user