zed-ets-language-server 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +74 -13
- package/lib/lib-expander.js +11 -3
- package/lib/sdk-discovery.js +182 -0
- package/lib/sdk-discovery.test.js +252 -0
- package/package.json +3 -3
- package/tests/integration/kit-diagnostics.test.js +232 -0
- package/tests/integration/lsp-server.test.js +148 -68
- package/tests/integration/sdk-initialization.test.js +167 -0
- package/tests/mocks/mock-ets-server.js +4 -2
package/index.js
CHANGED
|
@@ -6,7 +6,8 @@ import os from 'node:os';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { logger } from './lib/logger.js';
|
|
8
8
|
import { parse } from './lib/data-parser.js';
|
|
9
|
-
import { listHelperPaths } from './lib/lib-expander.js'
|
|
9
|
+
import { listHelperPaths } from './lib/lib-expander.js';
|
|
10
|
+
import { resolveHmsSdkPath, resolveOhosSdkPath } from './lib/sdk-discovery.js';
|
|
10
11
|
|
|
11
12
|
// ETS language server path, passed by Rust extension process through environment variable
|
|
12
13
|
const etsLangServerPath = process.env.ETS_LANG_SERVER;
|
|
@@ -37,12 +38,32 @@ function ensurePlaceholderSdk() {
|
|
|
37
38
|
|
|
38
39
|
function detectTsdk() {
|
|
39
40
|
if (!etsLangServerPath) return undefined;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
// The extension installs ohos-typescript next to @arkts/language-server:
|
|
42
|
+
// <work dir>/node_modules/ohos-typescript/lib
|
|
43
|
+
// <work dir>/node_modules/@arkts/language-server/bin/ets-language-server.js
|
|
44
|
+
// Walk the ancestors of the server path and accept either layout so the
|
|
45
|
+
// detection also works for servers installed at other depths.
|
|
46
|
+
let dir = path.dirname(path.resolve(etsLangServerPath));
|
|
47
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
48
|
+
for (const candidate of [
|
|
49
|
+
path.join(dir, 'ohos-typescript', 'lib'),
|
|
50
|
+
path.join(dir, 'node_modules', 'ohos-typescript', 'lib'),
|
|
51
|
+
]) {
|
|
52
|
+
if (fs.existsSync(path.join(candidate, 'typescript.js'))) return candidate;
|
|
53
|
+
}
|
|
54
|
+
const parent = path.dirname(dir);
|
|
55
|
+
if (parent === dir) break;
|
|
56
|
+
dir = parent;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A usable tsdk must contain lib/typescript.js. The native TypeScript 7 line
|
|
62
|
+
// ships no compiler API there, and settings inherited from another machine may
|
|
63
|
+
// point at a half-installed directory; forwarding such a tsdk makes the server
|
|
64
|
+
// hang inside `initialize` without ever answering.
|
|
65
|
+
function isValidTsdk(dir) {
|
|
66
|
+
return Boolean(dir) && fs.existsSync(path.join(dir, 'typescript.js'));
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
async function main() {
|
|
@@ -109,8 +130,29 @@ async function main() {
|
|
|
109
130
|
initializationOptions.tsdk = process.env.ZED_ETS_TSDK || process.env.TSDK || detectTsdk();
|
|
110
131
|
logger.info(`No tsdk in initializationOptions; falling back to: ${initializationOptions.tsdk}`);
|
|
111
132
|
}
|
|
112
|
-
|
|
113
|
-
|
|
133
|
+
// A native TypeScript installation may not contain the JS compiler API.
|
|
134
|
+
if (!isValidTsdk(initializationOptions.tsdk)) {
|
|
135
|
+
const fallbackTsdk = detectTsdk();
|
|
136
|
+
if (fallbackTsdk) {
|
|
137
|
+
logger.error(`tsdk ${initializationOptions.tsdk} has no lib/typescript.js; falling back to ${fallbackTsdk}`);
|
|
138
|
+
initializationOptions.tsdk = fallbackTsdk;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let hasRealOhosSdk = false;
|
|
143
|
+
try {
|
|
144
|
+
const resolvedSdk = resolveOhosSdkPath({
|
|
145
|
+
configuredPath: initializationOptions.ohosSdkPath,
|
|
146
|
+
env: process.env,
|
|
147
|
+
});
|
|
148
|
+
if (resolvedSdk) {
|
|
149
|
+
initializationOptions.ohosSdkPath = resolvedSdk.path;
|
|
150
|
+
hasRealOhosSdk = true;
|
|
151
|
+
logger.info(`Using HarmonyOS SDK from ${resolvedSdk.source}: ${resolvedSdk.path}`);
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
logger.error(error.message);
|
|
155
|
+
initializationOptions.ohosSdkPath = undefined;
|
|
114
156
|
}
|
|
115
157
|
|
|
116
158
|
// The server cannot finish `initialize` without a tsdk (it fails loading
|
|
@@ -123,13 +165,32 @@ async function main() {
|
|
|
123
165
|
|
|
124
166
|
if (!initializationOptions.ohosSdkPath) {
|
|
125
167
|
initializationOptions.ohosSdkPath = ensurePlaceholderSdk();
|
|
126
|
-
logger.error('No
|
|
168
|
+
logger.error('No valid HarmonyOS SDK was found in LSP settings, environment variables, or standard DevEco Studio locations; using a placeholder SDK skeleton. ArkUI and @kit types will be unavailable until lsp.arkts-language-server.initialization_options.ohosSdkPath is set in Zed settings.');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const resolvedHmsSdk = resolveHmsSdkPath({
|
|
173
|
+
configuredPath: initializationOptions.hmsSdkPath,
|
|
174
|
+
ohosSdkPath: hasRealOhosSdk ? initializationOptions.ohosSdkPath : undefined,
|
|
175
|
+
env: process.env,
|
|
176
|
+
});
|
|
177
|
+
if (resolvedHmsSdk) {
|
|
178
|
+
initializationOptions.hmsSdkPath = resolvedHmsSdk.path;
|
|
179
|
+
logger.info(`Using HMS SDK from ${resolvedHmsSdk.source}: ${resolvedHmsSdk.path}`);
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
logger.error(error.message);
|
|
183
|
+
initializationOptions.hmsSdkPath = undefined;
|
|
127
184
|
}
|
|
128
185
|
|
|
129
|
-
const ohos = await listHelperPaths(
|
|
186
|
+
const ohos = await listHelperPaths(
|
|
187
|
+
initializationOptions.tsdk,
|
|
188
|
+
initializationOptions.ohosSdkPath,
|
|
189
|
+
initializationOptions.hmsSdkPath,
|
|
190
|
+
);
|
|
130
191
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
192
|
+
// Current servers read `ets`; retain the `ohos` alias for clients that
|
|
193
|
+
// still inspect the older configuration key.
|
|
133
194
|
const etsSpecialRequest = {
|
|
134
195
|
jsonrpc: '2.0',
|
|
135
196
|
id: `zed-ets-wrapper-${Date.now()}`,
|
package/lib/lib-expander.js
CHANGED
|
@@ -73,22 +73,30 @@ export async function listLibs(dirPath) {
|
|
|
73
73
|
return await getFilesByPattern(dirPath, /d\.ts$/i);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export async function listHelperPaths(tsDir, harmonyDir) {
|
|
76
|
+
export async function listHelperPaths(tsDir, harmonyDir, hmsDir) {
|
|
77
77
|
const etsComponentPath = path.join(harmonyDir, '/ets/component');
|
|
78
78
|
const etsLoaderConfigPath = path.join(harmonyDir, '/ets/build-tools/ets-loader/tsconfig.json');
|
|
79
79
|
const etsLoaderPath = path.join(harmonyDir, '/ets/build-tools/ets-loader');
|
|
80
80
|
const etsLoaderLibs = await listLibs(path.join(etsLoaderPath, '/declarations'));
|
|
81
81
|
|
|
82
|
+
const modulePaths = ["./api/*", "./kits/*", "./arkts/*"];
|
|
83
|
+
if (hmsDir) {
|
|
84
|
+
modulePaths.push(path.join(hmsDir, 'ets', 'api', '*'));
|
|
85
|
+
modulePaths.push(path.join(hmsDir, 'ets', 'kits', '*'));
|
|
86
|
+
}
|
|
87
|
+
|
|
82
88
|
return {
|
|
83
89
|
sdkPath: harmonyDir,
|
|
90
|
+
hmsPath: hmsDir,
|
|
91
|
+
hmsSdkPath: hmsDir,
|
|
84
92
|
etsComponentPath,
|
|
85
93
|
etsLoaderConfigPath,
|
|
86
94
|
etsLoaderPath,
|
|
87
95
|
baseUrl: path.join(harmonyDir, '/ets'),
|
|
88
96
|
lib: [...(await listLibs(tsDir)), ...(await listLibs(etsComponentPath)), ...etsLoaderLibs],
|
|
89
97
|
"paths": {
|
|
90
|
-
"*":
|
|
98
|
+
"*": modulePaths,
|
|
91
99
|
"@internal/full/*": ["./api/@internal/full/*"]
|
|
92
100
|
},
|
|
93
101
|
};
|
|
94
|
-
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const ENVIRONMENT_VARIABLES = [
|
|
6
|
+
'ZED_ETS_OHOS_SDK_PATH',
|
|
7
|
+
'OHOS_SDK_PATH',
|
|
8
|
+
'HARMONYOS_SDK_HOME',
|
|
9
|
+
'OPENHARMONY_SDK_HOME',
|
|
10
|
+
'DEVECO_SDK_HOME',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const REQUIRED_SDK_ENTRIES = [
|
|
14
|
+
{ relativePath: path.join('ets', 'kits'), type: 'directory' },
|
|
15
|
+
{ relativePath: path.join('ets', 'api'), type: 'directory' },
|
|
16
|
+
{
|
|
17
|
+
relativePath: path.join('ets', 'build-tools', 'ets-loader', 'tsconfig.json'),
|
|
18
|
+
type: 'file',
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const REQUIRED_HMS_ENTRIES = [
|
|
23
|
+
{ relativePath: path.join('ets', 'kits'), type: 'directory' },
|
|
24
|
+
{ relativePath: path.join('ets', 'api'), type: 'directory' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function hasRequiredEntries(candidate, entries) {
|
|
28
|
+
try {
|
|
29
|
+
return entries.every(({ relativePath, type }) => {
|
|
30
|
+
const entryPath = path.join(candidate, relativePath);
|
|
31
|
+
const stats = fs.statSync(entryPath);
|
|
32
|
+
const accessMode = type === 'directory'
|
|
33
|
+
? fs.constants.R_OK | fs.constants.X_OK
|
|
34
|
+
: fs.constants.R_OK;
|
|
35
|
+
fs.accessSync(entryPath, accessMode);
|
|
36
|
+
return type === 'directory' ? stats.isDirectory() : stats.isFile();
|
|
37
|
+
});
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isValidOhosSdkPath(candidate) {
|
|
44
|
+
return hasRequiredEntries(candidate, REQUIRED_SDK_ENTRIES);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isValidHmsSdkPath(candidate) {
|
|
48
|
+
return hasRequiredEntries(candidate, REQUIRED_HMS_ENTRIES);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function expandHome(candidate, homeDirectory) {
|
|
52
|
+
return candidate.startsWith('~/')
|
|
53
|
+
? path.join(homeDirectory, candidate.slice(2))
|
|
54
|
+
: candidate;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeCandidate(candidate, homeDirectory = os.homedir()) {
|
|
58
|
+
if (!candidate) return undefined;
|
|
59
|
+
|
|
60
|
+
const expanded = expandHome(candidate, homeDirectory);
|
|
61
|
+
const normalized = path.resolve(expanded);
|
|
62
|
+
const possibleRoots = [
|
|
63
|
+
normalized,
|
|
64
|
+
path.join(normalized, 'openharmony'),
|
|
65
|
+
path.join(normalized, 'default', 'openharmony'),
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const versionDirectories = fs
|
|
70
|
+
.readdirSync(normalized, { withFileTypes: true })
|
|
71
|
+
.filter((entry) => entry.isDirectory() && entry.name !== 'default')
|
|
72
|
+
.map((entry) => entry.name)
|
|
73
|
+
.sort((left, right) => right.localeCompare(left, undefined, { numeric: true }));
|
|
74
|
+
for (const versionDirectory of versionDirectories) {
|
|
75
|
+
possibleRoots.push(path.join(normalized, versionDirectory, 'openharmony'));
|
|
76
|
+
possibleRoots.push(path.join(normalized, versionDirectory));
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Missing or unreadable candidates are ignored during discovery.
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return possibleRoots.find(isValidOhosSdkPath);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function defaultCandidates(platform, homeDirectory, env) {
|
|
86
|
+
const candidates = [];
|
|
87
|
+
const studioHome = env.DEVECO_STUDIO_HOME || env.DEVECO_HOME;
|
|
88
|
+
if (studioHome) {
|
|
89
|
+
candidates.push(path.join(studioHome, 'sdk'));
|
|
90
|
+
candidates.push(path.join(studioHome, 'Contents', 'sdk'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (platform === 'darwin') {
|
|
94
|
+
candidates.push(path.join(homeDirectory, 'Library', 'OpenHarmony', 'Sdk'));
|
|
95
|
+
candidates.push(path.join(homeDirectory, 'Library', 'Huawei', 'Sdk'));
|
|
96
|
+
candidates.push('/Applications/DevEco-Studio.app/Contents/sdk');
|
|
97
|
+
candidates.push(path.join(homeDirectory, 'Applications', 'DevEco-Studio.app', 'Contents', 'sdk'));
|
|
98
|
+
} else if (platform === 'win32') {
|
|
99
|
+
if (env.ProgramFiles) {
|
|
100
|
+
candidates.push(path.join(env.ProgramFiles, 'Huawei', 'DevEco Studio', 'sdk'));
|
|
101
|
+
}
|
|
102
|
+
if (env.LOCALAPPDATA) {
|
|
103
|
+
candidates.push(path.join(env.LOCALAPPDATA, 'Huawei', 'DevEcoStudio', 'sdk'));
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
candidates.push('/opt/DevEco-Studio/sdk');
|
|
107
|
+
candidates.push(path.join(homeDirectory, 'DevEco-Studio', 'sdk'));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return candidates;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function resolveOhosSdkPath({
|
|
114
|
+
configuredPath,
|
|
115
|
+
env = process.env,
|
|
116
|
+
platform = process.platform,
|
|
117
|
+
homeDirectory = os.homedir(),
|
|
118
|
+
candidates,
|
|
119
|
+
} = {}) {
|
|
120
|
+
if (configuredPath) {
|
|
121
|
+
const resolved = normalizeCandidate(configuredPath, homeDirectory);
|
|
122
|
+
if (!resolved) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Invalid HarmonyOS SDK path: ${configuredPath}. Expected ets/kits, ets/api, and ets/build-tools/ets-loader/tsconfig.json.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return { path: resolved, source: 'settings' };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const variableName of ENVIRONMENT_VARIABLES) {
|
|
131
|
+
const resolved = normalizeCandidate(env[variableName], homeDirectory);
|
|
132
|
+
if (resolved) {
|
|
133
|
+
return { path: resolved, source: variableName };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const searchCandidates = candidates ?? defaultCandidates(platform, homeDirectory, env);
|
|
138
|
+
for (const candidate of searchCandidates) {
|
|
139
|
+
const resolved = normalizeCandidate(candidate, homeDirectory);
|
|
140
|
+
if (resolved) {
|
|
141
|
+
return { path: resolved, source: 'auto-detected' };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function resolveHmsSdkPath({
|
|
149
|
+
configuredPath,
|
|
150
|
+
ohosSdkPath,
|
|
151
|
+
env = process.env,
|
|
152
|
+
homeDirectory = os.homedir(),
|
|
153
|
+
} = {}) {
|
|
154
|
+
const candidates = configuredPath
|
|
155
|
+
? [configuredPath, path.join(configuredPath, 'hms'), path.join(configuredPath, 'default', 'hms')]
|
|
156
|
+
: [
|
|
157
|
+
env.ZED_ETS_HMS_SDK_PATH,
|
|
158
|
+
env.HMS_SDK_PATH,
|
|
159
|
+
ohosSdkPath && path.join(path.dirname(ohosSdkPath), 'hms'),
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
for (const candidate of candidates) {
|
|
163
|
+
if (!candidate) continue;
|
|
164
|
+
const normalized = path.resolve(expandHome(candidate, homeDirectory));
|
|
165
|
+
if (isValidHmsSdkPath(normalized)) {
|
|
166
|
+
return {
|
|
167
|
+
path: normalized,
|
|
168
|
+
source: configuredPath ? 'settings' : 'auto-detected',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (configuredPath) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Invalid HMS SDK path: ${configuredPath}. Expected ets/kits and ets/api.`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export { isValidHmsSdkPath, isValidOhosSdkPath };
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { listHelperPaths } from './lib-expander.js';
|
|
6
|
+
import { resolveHmsSdkPath, resolveOhosSdkPath } from './sdk-discovery.js';
|
|
7
|
+
|
|
8
|
+
const temporaryDirectories = [];
|
|
9
|
+
|
|
10
|
+
async function createSdkRoot(directory, relativePath) {
|
|
11
|
+
const sdkRoot = path.join(directory, relativePath);
|
|
12
|
+
await fs.mkdir(path.join(sdkRoot, 'ets', 'api'), { recursive: true });
|
|
13
|
+
await fs.mkdir(path.join(sdkRoot, 'ets', 'kits'), { recursive: true });
|
|
14
|
+
await fs.mkdir(path.join(sdkRoot, 'ets', 'component'), { recursive: true });
|
|
15
|
+
await fs.mkdir(path.join(sdkRoot, 'ets', 'build-tools', 'ets-loader', 'declarations'), {
|
|
16
|
+
recursive: true,
|
|
17
|
+
});
|
|
18
|
+
await fs.writeFile(
|
|
19
|
+
path.join(sdkRoot, 'ets', 'build-tools', 'ets-loader', 'tsconfig.json'),
|
|
20
|
+
'{}\n',
|
|
21
|
+
);
|
|
22
|
+
await fs.writeFile(
|
|
23
|
+
path.join(sdkRoot, 'ets', 'kits', '@kit.AbilityKit.d.ts'),
|
|
24
|
+
'export declare class UIAbility {}\n',
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return sdkRoot;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function createSdkFixture() {
|
|
31
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'zed-arkts-sdk-'));
|
|
32
|
+
temporaryDirectories.push(directory);
|
|
33
|
+
const sdkRoot = await createSdkRoot(directory, path.join('sdk', 'default', 'openharmony'));
|
|
34
|
+
return { directory, sdkRoot };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function createHmsSdkRoot(ohosSdkRoot) {
|
|
38
|
+
const hmsSdkRoot = path.join(path.dirname(ohosSdkRoot), 'hms');
|
|
39
|
+
await fs.mkdir(path.join(hmsSdkRoot, 'ets', 'api'), { recursive: true });
|
|
40
|
+
await fs.mkdir(path.join(hmsSdkRoot, 'ets', 'kits'), { recursive: true });
|
|
41
|
+
await fs.writeFile(
|
|
42
|
+
path.join(hmsSdkRoot, 'ets', 'kits', '@kit.TestHmsKit.d.ts'),
|
|
43
|
+
'export declare const hmsApi: string;\n',
|
|
44
|
+
);
|
|
45
|
+
return hmsSdkRoot;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
afterEach(async () => {
|
|
49
|
+
await Promise.all(
|
|
50
|
+
temporaryDirectories.splice(0).map((directory) =>
|
|
51
|
+
fs.rm(directory, { recursive: true, force: true }),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('HarmonyOS SDK discovery', () => {
|
|
57
|
+
it('normalizes a DevEco SDK directory to its OpenHarmony root', async () => {
|
|
58
|
+
const { directory, sdkRoot } = await createSdkFixture();
|
|
59
|
+
|
|
60
|
+
const result = resolveOhosSdkPath({
|
|
61
|
+
configuredPath: path.join(directory, 'sdk', 'default'),
|
|
62
|
+
env: {},
|
|
63
|
+
candidates: [],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(result).toEqual({ path: sdkRoot, source: 'settings' });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('prefers the explicitly configured SDK over environment values', async () => {
|
|
70
|
+
const configured = await createSdkFixture();
|
|
71
|
+
const fromEnvironment = await createSdkFixture();
|
|
72
|
+
|
|
73
|
+
const result = resolveOhosSdkPath({
|
|
74
|
+
configuredPath: configured.sdkRoot,
|
|
75
|
+
env: { ZED_ETS_OHOS_SDK_PATH: fromEnvironment.sdkRoot },
|
|
76
|
+
candidates: [],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(result).toEqual({ path: configured.sdkRoot, source: 'settings' });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('expands a configured OpenHarmony SDK path relative to the supplied home', async () => {
|
|
83
|
+
const homeDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'zed-arkts-tilde-ohos-'));
|
|
84
|
+
temporaryDirectories.push(homeDirectory);
|
|
85
|
+
const sdkRoot = await createSdkRoot(
|
|
86
|
+
homeDirectory,
|
|
87
|
+
path.join('HarmonySdk', 'default', 'openharmony'),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const result = resolveOhosSdkPath({
|
|
91
|
+
configuredPath: '~/HarmonySdk',
|
|
92
|
+
env: {},
|
|
93
|
+
candidates: [],
|
|
94
|
+
homeDirectory,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(result).toEqual({ path: sdkRoot, source: 'settings' });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('discovers the SDK bundled with DevEco Studio', async () => {
|
|
101
|
+
const { directory, sdkRoot } = await createSdkFixture();
|
|
102
|
+
|
|
103
|
+
const result = resolveOhosSdkPath({
|
|
104
|
+
env: {},
|
|
105
|
+
candidates: [path.join(directory, 'sdk')],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(result).toEqual({ path: sdkRoot, source: 'auto-detected' });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it.each([
|
|
112
|
+
['OpenHarmony', path.join('Library', 'OpenHarmony', 'Sdk')],
|
|
113
|
+
['Huawei', path.join('Library', 'Huawei', 'Sdk')],
|
|
114
|
+
])('discovers the macOS %s user SDK directory', async (_vendor, sdkDirectory) => {
|
|
115
|
+
const homeDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'zed-arkts-home-sdk-'));
|
|
116
|
+
temporaryDirectories.push(homeDirectory);
|
|
117
|
+
const sdkRoot = await createSdkRoot(
|
|
118
|
+
homeDirectory,
|
|
119
|
+
path.join(sdkDirectory, 'default', 'openharmony'),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const result = resolveOhosSdkPath({
|
|
123
|
+
env: {},
|
|
124
|
+
platform: 'darwin',
|
|
125
|
+
homeDirectory,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(result).toEqual({ path: sdkRoot, source: 'auto-detected' });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('selects the newest installed version when no default SDK exists', async () => {
|
|
132
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'zed-arkts-versioned-sdk-'));
|
|
133
|
+
temporaryDirectories.push(directory);
|
|
134
|
+
await createSdkRoot(directory, path.join('sdk', '12', 'openharmony'));
|
|
135
|
+
const newestSdk = await createSdkRoot(directory, path.join('sdk', '24', 'openharmony'));
|
|
136
|
+
|
|
137
|
+
const result = resolveOhosSdkPath({
|
|
138
|
+
env: {},
|
|
139
|
+
candidates: [path.join(directory, 'sdk')],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(result).toEqual({ path: newestSdk, source: 'auto-detected' });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('rejects a path that does not contain ArkTS SDK declarations', async () => {
|
|
146
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'zed-arkts-invalid-sdk-'));
|
|
147
|
+
temporaryDirectories.push(directory);
|
|
148
|
+
|
|
149
|
+
expect(() =>
|
|
150
|
+
resolveOhosSdkPath({
|
|
151
|
+
configuredPath: directory,
|
|
152
|
+
env: {},
|
|
153
|
+
candidates: [],
|
|
154
|
+
}),
|
|
155
|
+
).toThrow(/ets\/kits.*ets-loader\/tsconfig\.json/);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('rejects SDK entries with the wrong file types', async () => {
|
|
159
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'zed-arkts-malformed-sdk-'));
|
|
160
|
+
temporaryDirectories.push(directory);
|
|
161
|
+
const sdkRoot = path.join(directory, 'openharmony');
|
|
162
|
+
await fs.mkdir(path.join(sdkRoot, 'ets', 'api'), { recursive: true });
|
|
163
|
+
await fs.writeFile(path.join(sdkRoot, 'ets', 'kits'), 'not a directory\n');
|
|
164
|
+
await fs.mkdir(path.join(sdkRoot, 'ets', 'build-tools', 'ets-loader', 'tsconfig.json'), {
|
|
165
|
+
recursive: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(() =>
|
|
169
|
+
resolveOhosSdkPath({ configuredPath: sdkRoot, env: {}, candidates: [] }),
|
|
170
|
+
).toThrow(/Invalid HarmonyOS SDK path/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it.runIf(process.platform !== 'win32')('rejects SDK directories that cannot be traversed', async () => {
|
|
174
|
+
const { sdkRoot } = await createSdkFixture();
|
|
175
|
+
const apiDirectory = path.join(sdkRoot, 'ets', 'api');
|
|
176
|
+
await fs.chmod(apiDirectory, 0o400);
|
|
177
|
+
|
|
178
|
+
expect(() =>
|
|
179
|
+
resolveOhosSdkPath({ configuredPath: sdkRoot, env: {}, candidates: [] }),
|
|
180
|
+
).toThrow(/Invalid HarmonyOS SDK path/);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('discovers a sibling HMS SDK and adds it to module resolution paths', async () => {
|
|
184
|
+
const { directory, sdkRoot } = await createSdkFixture();
|
|
185
|
+
const hmsSdkRoot = await createHmsSdkRoot(sdkRoot);
|
|
186
|
+
const tsdk = path.join(directory, 'typescript', 'lib');
|
|
187
|
+
await fs.mkdir(tsdk, { recursive: true });
|
|
188
|
+
await fs.writeFile(path.join(tsdk, 'lib.es5.d.ts'), 'interface Object {}\n');
|
|
189
|
+
|
|
190
|
+
const hmsResult = resolveHmsSdkPath({ ohosSdkPath: sdkRoot, env: {} });
|
|
191
|
+
const helperPaths = await listHelperPaths(tsdk, sdkRoot, hmsResult.path);
|
|
192
|
+
|
|
193
|
+
expect(hmsResult).toEqual({ path: hmsSdkRoot, source: 'auto-detected' });
|
|
194
|
+
expect(helperPaths.hmsSdkPath).toBe(hmsSdkRoot);
|
|
195
|
+
expect(helperPaths.paths['*']).toContain(path.join(hmsSdkRoot, 'ets', 'kits', '*'));
|
|
196
|
+
expect(helperPaths.paths['*']).toContain(path.join(hmsSdkRoot, 'ets', 'api', '*'));
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('expands a configured HMS SDK path relative to the supplied home', async () => {
|
|
200
|
+
const homeDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'zed-arkts-tilde-hms-'));
|
|
201
|
+
temporaryDirectories.push(homeDirectory);
|
|
202
|
+
const hmsSdkRoot = path.join(homeDirectory, 'HarmonySdk', 'default', 'hms');
|
|
203
|
+
await fs.mkdir(path.join(hmsSdkRoot, 'ets', 'api'), { recursive: true });
|
|
204
|
+
await fs.mkdir(path.join(hmsSdkRoot, 'ets', 'kits'), { recursive: true });
|
|
205
|
+
|
|
206
|
+
const result = resolveHmsSdkPath({
|
|
207
|
+
configuredPath: '~/HarmonySdk/default/hms',
|
|
208
|
+
env: {},
|
|
209
|
+
homeDirectory,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(result).toEqual({ path: hmsSdkRoot, source: 'settings' });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('resolves OpenHarmony and HMS kit modules through the generated paths', async () => {
|
|
216
|
+
const typescriptModule = await import('ohos-typescript');
|
|
217
|
+
const ts = typescriptModule.default ?? typescriptModule;
|
|
218
|
+
const { directory, sdkRoot } = await createSdkFixture();
|
|
219
|
+
const hmsSdkRoot = await createHmsSdkRoot(sdkRoot);
|
|
220
|
+
const helperPaths = await listHelperPaths(
|
|
221
|
+
path.join(process.cwd(), 'node_modules', 'ohos-typescript', 'lib'),
|
|
222
|
+
sdkRoot,
|
|
223
|
+
hmsSdkRoot,
|
|
224
|
+
);
|
|
225
|
+
const compilerOptions = {
|
|
226
|
+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
|
227
|
+
baseUrl: helperPaths.baseUrl,
|
|
228
|
+
paths: helperPaths.paths,
|
|
229
|
+
};
|
|
230
|
+
const containingFile = path.join(directory, 'entry', 'src', 'main', 'ets', 'Probe.ets');
|
|
231
|
+
|
|
232
|
+
const abilityKit = ts.resolveModuleName(
|
|
233
|
+
'@kit.AbilityKit',
|
|
234
|
+
containingFile,
|
|
235
|
+
compilerOptions,
|
|
236
|
+
ts.sys,
|
|
237
|
+
).resolvedModule;
|
|
238
|
+
const hmsKit = ts.resolveModuleName(
|
|
239
|
+
'@kit.TestHmsKit',
|
|
240
|
+
containingFile,
|
|
241
|
+
compilerOptions,
|
|
242
|
+
ts.sys,
|
|
243
|
+
).resolvedModule;
|
|
244
|
+
|
|
245
|
+
expect(abilityKit?.resolvedFileName).toBe(
|
|
246
|
+
path.join(sdkRoot, 'ets', 'kits', '@kit.AbilityKit.d.ts'),
|
|
247
|
+
);
|
|
248
|
+
expect(hmsKit?.resolvedFileName).toBe(
|
|
249
|
+
path.join(hmsSdkRoot, 'ets', 'kits', '@kit.TestHmsKit.d.ts'),
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zed-ets-language-server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "ETS language server wrapper for Zed ArkTS extension.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
"author": "liuyanghejerry <liuyanghejerry@126.com>",
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@arkts/language-server": "^1.
|
|
20
|
+
"@arkts/language-server": "^1.3.10"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"vitest": "^4.
|
|
23
|
+
"vitest": "^4.1.11"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const testDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
let fixtureDirectory;
|
|
10
|
+
let projectDirectory;
|
|
11
|
+
let sourcePath;
|
|
12
|
+
let serverProcess;
|
|
13
|
+
let languageServerVersion;
|
|
14
|
+
let stdoutBuffer = Buffer.alloc(0);
|
|
15
|
+
const messages = [];
|
|
16
|
+
const waiters = [];
|
|
17
|
+
|
|
18
|
+
function send(message) {
|
|
19
|
+
const body = Buffer.from(JSON.stringify(message));
|
|
20
|
+
serverProcess.stdin.write(
|
|
21
|
+
Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`), body]),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function dispatch(message) {
|
|
26
|
+
messages.push(message);
|
|
27
|
+
|
|
28
|
+
if (message.method && message.id !== undefined) {
|
|
29
|
+
let result = null;
|
|
30
|
+
if (message.method === 'workspace/configuration') {
|
|
31
|
+
result = (message.params?.items ?? []).map(() => null);
|
|
32
|
+
} else if (message.method === 'workspace/workspaceFolders') {
|
|
33
|
+
result = [{ uri: pathToFileURL(projectDirectory).href, name: 'project' }];
|
|
34
|
+
}
|
|
35
|
+
send({ jsonrpc: '2.0', id: message.id, result });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (let index = waiters.length - 1; index >= 0; index -= 1) {
|
|
39
|
+
if (!waiters[index].predicate(message)) continue;
|
|
40
|
+
const [waiter] = waiters.splice(index, 1);
|
|
41
|
+
clearTimeout(waiter.timer);
|
|
42
|
+
waiter.resolve(message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function collectMessages(chunk) {
|
|
47
|
+
stdoutBuffer = Buffer.concat([stdoutBuffer, chunk]);
|
|
48
|
+
while (true) {
|
|
49
|
+
const headerEnd = stdoutBuffer.indexOf('\r\n\r\n');
|
|
50
|
+
if (headerEnd === -1) return;
|
|
51
|
+
const header = stdoutBuffer.subarray(0, headerEnd).toString('ascii');
|
|
52
|
+
const lengthMatch = header.match(/Content-Length:\s*(\d+)/i);
|
|
53
|
+
if (!lengthMatch) throw new Error('LSP response is missing Content-Length');
|
|
54
|
+
const bodyLength = Number(lengthMatch[1]);
|
|
55
|
+
const bodyStart = headerEnd + 4;
|
|
56
|
+
if (stdoutBuffer.length < bodyStart + bodyLength) return;
|
|
57
|
+
const body = stdoutBuffer.subarray(bodyStart, bodyStart + bodyLength);
|
|
58
|
+
stdoutBuffer = stdoutBuffer.subarray(bodyStart + bodyLength);
|
|
59
|
+
dispatch(JSON.parse(body.toString('utf8')));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function waitForMessage(predicate, timeout = 30000) {
|
|
64
|
+
const existing = messages.find(predicate);
|
|
65
|
+
if (existing) return Promise.resolve(existing);
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const timer = setTimeout(() => reject(new Error('Timed out waiting for LSP message')), timeout);
|
|
68
|
+
waiters.push({ predicate, resolve, timer });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
beforeAll(async () => {
|
|
73
|
+
fixtureDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'zed-arkts-real-lsp-'));
|
|
74
|
+
projectDirectory = path.join(fixtureDirectory, 'project');
|
|
75
|
+
const sdkDirectory = path.join(fixtureDirectory, 'studio', 'Contents', 'sdk', 'default');
|
|
76
|
+
const ohosSdk = path.join(sdkDirectory, 'openharmony');
|
|
77
|
+
const hmsSdk = path.join(sdkDirectory, 'hms');
|
|
78
|
+
sourcePath = path.join(projectDirectory, 'entry', 'src', 'main', 'ets', 'Probe.ets');
|
|
79
|
+
|
|
80
|
+
await Promise.all([
|
|
81
|
+
fs.mkdir(path.dirname(sourcePath), { recursive: true }),
|
|
82
|
+
fs.mkdir(path.join(ohosSdk, 'ets', 'api'), { recursive: true }),
|
|
83
|
+
fs.mkdir(path.join(ohosSdk, 'ets', 'kits'), { recursive: true }),
|
|
84
|
+
fs.mkdir(path.join(ohosSdk, 'ets', 'component'), { recursive: true }),
|
|
85
|
+
fs.mkdir(path.join(ohosSdk, 'ets', 'build-tools', 'ets-loader', 'declarations'), {
|
|
86
|
+
recursive: true,
|
|
87
|
+
}),
|
|
88
|
+
fs.mkdir(path.join(hmsSdk, 'ets', 'api'), { recursive: true }),
|
|
89
|
+
fs.mkdir(path.join(hmsSdk, 'ets', 'kits'), { recursive: true }),
|
|
90
|
+
]);
|
|
91
|
+
await Promise.all([
|
|
92
|
+
fs.writeFile(path.join(ohosSdk, 'ets', 'build-tools', 'ets-loader', 'tsconfig.json'), '{}\n'),
|
|
93
|
+
fs.writeFile(
|
|
94
|
+
path.join(ohosSdk, 'ets', 'kits', '@kit.AbilityKit.d.ts'),
|
|
95
|
+
'export declare class UIAbility {}\n',
|
|
96
|
+
),
|
|
97
|
+
fs.writeFile(
|
|
98
|
+
path.join(hmsSdk, 'ets', 'kits', '@kit.TestHmsKit.d.ts'),
|
|
99
|
+
'export declare const hmsApi: string;\n',
|
|
100
|
+
),
|
|
101
|
+
fs.writeFile(
|
|
102
|
+
path.join(projectDirectory, 'build-profile.json5'),
|
|
103
|
+
'{ app: { products: [{ name: "default" }] }, modules: [{ name: "entry", srcPath: "./entry" }] }\n',
|
|
104
|
+
),
|
|
105
|
+
fs.writeFile(
|
|
106
|
+
path.join(projectDirectory, 'oh-package.json5'),
|
|
107
|
+
'{ name: "probe", version: "1.0.0" }\n',
|
|
108
|
+
),
|
|
109
|
+
fs.writeFile(
|
|
110
|
+
path.join(projectDirectory, 'entry', 'build-profile.json5'),
|
|
111
|
+
'{ apiType: "stageMode", buildOption: {} }\n',
|
|
112
|
+
),
|
|
113
|
+
fs.writeFile(
|
|
114
|
+
sourcePath,
|
|
115
|
+
"import { UIAbility } from '@kit.AbilityKit';\n" +
|
|
116
|
+
"import { hmsApi } from '@kit.TestHmsKit';\n" +
|
|
117
|
+
"import { missingApi } from '@kit.DoesNotExist';\n" +
|
|
118
|
+
'export const probe: UIAbility | string = hmsApi;\n' +
|
|
119
|
+
'export const missingProbe = missingApi;\n',
|
|
120
|
+
),
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
const wrapperPath = path.join(testDirectory, '..', '..', 'index.js');
|
|
124
|
+
const languageServerPath = path.join(
|
|
125
|
+
testDirectory,
|
|
126
|
+
'..',
|
|
127
|
+
'..',
|
|
128
|
+
'node_modules',
|
|
129
|
+
'@arkts',
|
|
130
|
+
'language-server',
|
|
131
|
+
'bin',
|
|
132
|
+
'ets-language-server.js',
|
|
133
|
+
);
|
|
134
|
+
const languageServerPackage = JSON.parse(
|
|
135
|
+
await fs.readFile(
|
|
136
|
+
path.join(testDirectory, '..', '..', 'node_modules', '@arkts', 'language-server', 'package.json'),
|
|
137
|
+
'utf8',
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
languageServerVersion = languageServerPackage.version;
|
|
141
|
+
const env = { ...process.env };
|
|
142
|
+
for (const variableName of [
|
|
143
|
+
'ZED_ETS_OHOS_SDK_PATH',
|
|
144
|
+
'OHOS_SDK_PATH',
|
|
145
|
+
'HARMONYOS_SDK_HOME',
|
|
146
|
+
'OPENHARMONY_SDK_HOME',
|
|
147
|
+
'DEVECO_SDK_HOME',
|
|
148
|
+
'ZED_ETS_HMS_SDK_PATH',
|
|
149
|
+
'HMS_SDK_PATH',
|
|
150
|
+
'ZED_ETS_TSDK',
|
|
151
|
+
'TSDK',
|
|
152
|
+
]) {
|
|
153
|
+
delete env[variableName];
|
|
154
|
+
}
|
|
155
|
+
Object.assign(env, {
|
|
156
|
+
ETS_LANG_SERVER: languageServerPath,
|
|
157
|
+
DEVECO_STUDIO_HOME: path.join(fixtureDirectory, 'studio'),
|
|
158
|
+
});
|
|
159
|
+
serverProcess = spawn(process.execPath, [wrapperPath], {
|
|
160
|
+
cwd: projectDirectory,
|
|
161
|
+
env,
|
|
162
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
163
|
+
});
|
|
164
|
+
serverProcess.stdout.on('data', collectMessages);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
afterAll(async () => {
|
|
168
|
+
if (serverProcess && serverProcess.exitCode === null) {
|
|
169
|
+
await new Promise((resolve) => {
|
|
170
|
+
const timeout = setTimeout(() => serverProcess.kill('SIGKILL'), 5000);
|
|
171
|
+
serverProcess.once('exit', () => {
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
resolve();
|
|
174
|
+
});
|
|
175
|
+
serverProcess.kill();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
await fs.rm(fixtureDirectory, { recursive: true, force: true });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('real ArkTS language server Kit diagnostics', () => {
|
|
182
|
+
it('resolves OpenHarmony and HMS Kit modules with language server 1.3.x', async () => {
|
|
183
|
+
expect(languageServerVersion).toMatch(/^1\.3\./);
|
|
184
|
+
const rootUri = pathToFileURL(projectDirectory).href;
|
|
185
|
+
send({
|
|
186
|
+
jsonrpc: '2.0',
|
|
187
|
+
id: 1,
|
|
188
|
+
method: 'initialize',
|
|
189
|
+
params: {
|
|
190
|
+
processId: process.pid,
|
|
191
|
+
rootUri,
|
|
192
|
+
workspaceFolders: [{ uri: rootUri, name: 'project' }],
|
|
193
|
+
capabilities: {
|
|
194
|
+
workspace: { configuration: true, workspaceFolders: true },
|
|
195
|
+
textDocument: { publishDiagnostics: {} },
|
|
196
|
+
},
|
|
197
|
+
initializationOptions: {},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
const initializeResponse = await waitForMessage((message) => message.id === 1);
|
|
201
|
+
expect(initializeResponse.error).toBeUndefined();
|
|
202
|
+
send({ jsonrpc: '2.0', method: 'initialized', params: {} });
|
|
203
|
+
|
|
204
|
+
const uri = pathToFileURL(sourcePath).href;
|
|
205
|
+
send({
|
|
206
|
+
jsonrpc: '2.0',
|
|
207
|
+
method: 'textDocument/didOpen',
|
|
208
|
+
params: {
|
|
209
|
+
textDocument: {
|
|
210
|
+
uri,
|
|
211
|
+
languageId: 'ets',
|
|
212
|
+
version: 1,
|
|
213
|
+
text: await fs.readFile(sourcePath, 'utf8'),
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
const diagnostics = await waitForMessage(
|
|
218
|
+
(message) =>
|
|
219
|
+
message.method === 'textDocument/publishDiagnostics' &&
|
|
220
|
+
message.params.uri === uri &&
|
|
221
|
+
message.params.diagnostics.some((diagnostic) => Number(diagnostic.code) === 2307),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const unresolvedModules = diagnostics.params.diagnostics.filter(
|
|
225
|
+
(diagnostic) => Number(diagnostic.code) === 2307,
|
|
226
|
+
);
|
|
227
|
+
expect(unresolvedModules).toHaveLength(1);
|
|
228
|
+
expect(unresolvedModules[0].message).toContain('@kit.DoesNotExist');
|
|
229
|
+
expect(unresolvedModules[0].message).not.toContain('@kit.AbilityKit');
|
|
230
|
+
expect(unresolvedModules[0].message).not.toContain('@kit.TestHmsKit');
|
|
231
|
+
}, 40000);
|
|
232
|
+
});
|
|
@@ -1,18 +1,36 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
|
+
import { mkdtempSync } from 'fs';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
3
5
|
import { fileURLToPath } from 'url';
|
|
4
6
|
import { dirname, join } from 'path';
|
|
5
7
|
|
|
6
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
9
|
const __dirname = dirname(__filename);
|
|
8
10
|
|
|
11
|
+
const serverPath = join(__dirname, '../../index.js');
|
|
12
|
+
const mockServerPath = join(__dirname, '../mocks/mock-ets-server.js');
|
|
13
|
+
const bundledTsdk = join(__dirname, '../../node_modules/ohos-typescript/lib');
|
|
14
|
+
|
|
15
|
+
// Hermetic env: never inherit ambient tsdk/sdk settings from the shell.
|
|
16
|
+
function baseEnv(overrides = {}) {
|
|
17
|
+
return {
|
|
18
|
+
...process.env,
|
|
19
|
+
ETS_LANG_SERVER: mockServerPath,
|
|
20
|
+
TSDK: '',
|
|
21
|
+
ZED_ETS_TSDK: '',
|
|
22
|
+
OHOS_SDK_PATH: '',
|
|
23
|
+
ZED_ETS_OHOS_SDK_PATH: '',
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
/**
|
|
10
29
|
* 创建 LSP 消息
|
|
11
30
|
*/
|
|
12
31
|
function createLSPMessage(content) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
return `Content-Length: ${contentLength}\r\n\r\n${json}`;
|
|
32
|
+
const contentLength = Buffer.byteLength(JSON.stringify(content));
|
|
33
|
+
return `Content-Length: ${contentLength}\r\n\r\n${JSON.stringify(content)}`;
|
|
16
34
|
}
|
|
17
35
|
|
|
18
36
|
/**
|
|
@@ -22,7 +40,7 @@ function parseLSPResponse(data) {
|
|
|
22
40
|
const text = data.toString();
|
|
23
41
|
const match = text.match(/Content-Length: (\d+)\r\n\r\n(.*)/s);
|
|
24
42
|
if (!match) return null;
|
|
25
|
-
|
|
43
|
+
|
|
26
44
|
try {
|
|
27
45
|
return JSON.parse(match[2]);
|
|
28
46
|
} catch (e) {
|
|
@@ -30,28 +48,77 @@ function parseLSPResponse(data) {
|
|
|
30
48
|
}
|
|
31
49
|
}
|
|
32
50
|
|
|
51
|
+
/**
|
|
52
|
+
* 等待特定响应
|
|
53
|
+
*/
|
|
54
|
+
function waitForResponse(responses, predicate, timeout = 2000) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const startTime = Date.now();
|
|
57
|
+
const checkInterval = setInterval(() => {
|
|
58
|
+
const response = responses.find(predicate);
|
|
59
|
+
if (response) {
|
|
60
|
+
clearInterval(checkInterval);
|
|
61
|
+
resolve(response);
|
|
62
|
+
} else if (Date.now() - startTime > timeout) {
|
|
63
|
+
clearInterval(checkInterval);
|
|
64
|
+
reject(new Error('Timeout waiting for response'));
|
|
65
|
+
}
|
|
66
|
+
}, 50);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function startWrapper(env) {
|
|
71
|
+
const serverProcess = spawn('node', [serverPath], {
|
|
72
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
73
|
+
env,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const responses = [];
|
|
77
|
+
let stdoutBuffer = Buffer.alloc(0);
|
|
78
|
+
serverProcess.stdout.on('data', (data) => {
|
|
79
|
+
stdoutBuffer = Buffer.concat([stdoutBuffer, data]);
|
|
80
|
+
// SDK discovery can make initialize responses span multiple pipe chunks.
|
|
81
|
+
while (true) {
|
|
82
|
+
const headerEnd = stdoutBuffer.indexOf('\r\n\r\n');
|
|
83
|
+
if (headerEnd < 0) return;
|
|
84
|
+
const header = stdoutBuffer.subarray(0, headerEnd).toString('ascii');
|
|
85
|
+
const lengthMatch = header.match(/Content-Length:\s*(\d+)/i);
|
|
86
|
+
if (!lengthMatch) throw new Error('Invalid LSP response header');
|
|
87
|
+
const bodyStart = headerEnd + 4;
|
|
88
|
+
const bodyEnd = bodyStart + Number(lengthMatch[1]);
|
|
89
|
+
if (stdoutBuffer.length < bodyEnd) return;
|
|
90
|
+
responses.push(JSON.parse(stdoutBuffer.subarray(bodyStart, bodyEnd).toString('utf8')));
|
|
91
|
+
stdoutBuffer = stdoutBuffer.subarray(bodyEnd);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
serverProcess.stderr.on('data', (data) => {
|
|
96
|
+
console.error(`LSP Server Error: ${data}`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return { serverProcess, responses };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function initializeMessage(id) {
|
|
103
|
+
return createLSPMessage({
|
|
104
|
+
jsonrpc: '2.0',
|
|
105
|
+
id,
|
|
106
|
+
method: 'initialize',
|
|
107
|
+
params: {
|
|
108
|
+
processId: null,
|
|
109
|
+
rootUri: null,
|
|
110
|
+
capabilities: {},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
33
115
|
describe('LSP Server Integration Tests', () => {
|
|
34
116
|
let serverProcess;
|
|
35
117
|
let responses = [];
|
|
118
|
+
let messageId = 1;
|
|
36
119
|
|
|
37
120
|
beforeAll(() => {
|
|
38
|
-
|
|
39
|
-
const serverPath = join(__dirname, '../../index.js');
|
|
40
|
-
serverProcess = spawn('node', [serverPath], {
|
|
41
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// 收集响应
|
|
45
|
-
serverProcess.stdout.on('data', (data) => {
|
|
46
|
-
const response = parseLSPResponse(data);
|
|
47
|
-
if (response) {
|
|
48
|
-
responses.push(response);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
serverProcess.stderr.on('data', (data) => {
|
|
53
|
-
console.error(`LSP Server Error: ${data}`);
|
|
54
|
-
});
|
|
121
|
+
({ serverProcess, responses } = startWrapper(baseEnv()));
|
|
55
122
|
});
|
|
56
123
|
|
|
57
124
|
afterAll(() => {
|
|
@@ -60,65 +127,78 @@ describe('LSP Server Integration Tests', () => {
|
|
|
60
127
|
}
|
|
61
128
|
});
|
|
62
129
|
|
|
63
|
-
it('should respond to initialize request', (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
capabilities: {}
|
|
72
|
-
}
|
|
73
|
-
};
|
|
130
|
+
it('should respond to initialize request', async () => {
|
|
131
|
+
serverProcess.stdin.write(initializeMessage(messageId));
|
|
132
|
+
const initResponse = await waitForResponse(responses, (r) => r.id === messageId);
|
|
133
|
+
messageId++;
|
|
134
|
+
|
|
135
|
+
expect(initResponse.result).toBeDefined();
|
|
136
|
+
expect(initResponse.result.capabilities).toBeDefined();
|
|
137
|
+
});
|
|
74
138
|
|
|
75
|
-
|
|
76
|
-
serverProcess.stdin.write(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
expect(initResponse).toBeDefined();
|
|
82
|
-
expect(initResponse.result).toBeDefined();
|
|
83
|
-
expect(initResponse.result.capabilities).toBeDefined();
|
|
84
|
-
done();
|
|
85
|
-
}, 1000);
|
|
139
|
+
it('should auto-detect the bundled tsdk when none is configured', async () => {
|
|
140
|
+
serverProcess.stdin.write(initializeMessage(messageId));
|
|
141
|
+
const initResponse = await waitForResponse(responses, (r) => r.id === messageId);
|
|
142
|
+
messageId++;
|
|
143
|
+
|
|
144
|
+
expect(initResponse.result.initializationOptions.typescript.tsdk).toBe(bundledTsdk);
|
|
86
145
|
});
|
|
87
146
|
|
|
88
|
-
it('should accept initialized notification', (
|
|
89
|
-
|
|
147
|
+
it('should accept initialized notification', async () => {
|
|
148
|
+
serverProcess.stdin.write(createLSPMessage({
|
|
90
149
|
jsonrpc: '2.0',
|
|
91
150
|
method: 'initialized',
|
|
92
|
-
params: {}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const message = createLSPMessage(initializedNotif);
|
|
96
|
-
serverProcess.stdin.write(message);
|
|
151
|
+
params: {},
|
|
152
|
+
}));
|
|
97
153
|
|
|
98
154
|
// 通知不需要响应,只需确保不崩溃
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
done();
|
|
102
|
-
}, 500);
|
|
155
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
156
|
+
expect(serverProcess.exitCode).toBeNull();
|
|
103
157
|
});
|
|
104
158
|
|
|
105
|
-
it('should handle shutdown request', (
|
|
106
|
-
|
|
159
|
+
it('should handle shutdown request', async () => {
|
|
160
|
+
serverProcess.stdin.write(createLSPMessage({
|
|
107
161
|
jsonrpc: '2.0',
|
|
108
162
|
id: 99,
|
|
109
163
|
method: 'shutdown',
|
|
110
|
-
params: null
|
|
111
|
-
};
|
|
164
|
+
params: null,
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
const shutdownResponse = await waitForResponse(responses, (r) => r.id === 99);
|
|
168
|
+
// 某些 LSP 服务器可能返回 null result
|
|
169
|
+
expect(shutdownResponse).toBeDefined();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('LSP tsdk fallback', () => {
|
|
174
|
+
// A tsdk without lib/typescript.js (e.g. the native TypeScript 7 line)
|
|
175
|
+
// used to hang the real server inside initialize. The wrapper must
|
|
176
|
+
// substitute the bundled ohos-typescript instead of forwarding it.
|
|
177
|
+
it('substitutes a broken TSDK env value with the bundled ohos-typescript', async () => {
|
|
178
|
+
const brokenTsdkDir = mkdtempSync(join(tmpdir(), 'zed-ets-broken-tsdk-'));
|
|
179
|
+
const { serverProcess, responses } = startWrapper(baseEnv({ TSDK: brokenTsdkDir }));
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
serverProcess.stdin.write(initializeMessage(1));
|
|
183
|
+
const initResponse = await waitForResponse(responses, (r) => r.id === 1);
|
|
184
|
+
|
|
185
|
+
expect(initResponse.result.initializationOptions.typescript.tsdk).toBe(bundledTsdk);
|
|
186
|
+
} finally {
|
|
187
|
+
serverProcess.kill();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('forwards a valid TSDK unchanged', async () => {
|
|
192
|
+
const { serverProcess, responses } = startWrapper(baseEnv({ TSDK: bundledTsdk }));
|
|
112
193
|
|
|
113
|
-
|
|
114
|
-
|
|
194
|
+
try {
|
|
195
|
+
serverProcess.stdin.write(initializeMessage(1));
|
|
196
|
+
const initResponse = await waitForResponse(responses, (r) => r.id === 1);
|
|
115
197
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
done();
|
|
121
|
-
}, 1000);
|
|
198
|
+
expect(initResponse.result.initializationOptions.typescript.tsdk).toBe(bundledTsdk);
|
|
199
|
+
} finally {
|
|
200
|
+
serverProcess.kill();
|
|
201
|
+
}
|
|
122
202
|
});
|
|
123
203
|
});
|
|
124
204
|
|
|
@@ -126,7 +206,7 @@ describe('LSP Message Protocol', () => {
|
|
|
126
206
|
it('should format messages correctly', () => {
|
|
127
207
|
const content = { jsonrpc: '2.0', method: 'test' };
|
|
128
208
|
const message = createLSPMessage(content);
|
|
129
|
-
|
|
209
|
+
|
|
130
210
|
expect(message).toContain('Content-Length:');
|
|
131
211
|
expect(message).toContain('\r\n\r\n');
|
|
132
212
|
expect(message).toContain(JSON.stringify(content));
|
|
@@ -140,7 +220,7 @@ describe('LSP Message Protocol', () => {
|
|
|
140
220
|
};
|
|
141
221
|
const data = createLSPMessage(mockResponse);
|
|
142
222
|
const parsed = parseLSPResponse(Buffer.from(data));
|
|
143
|
-
|
|
223
|
+
|
|
144
224
|
expect(parsed).toEqual(mockResponse);
|
|
145
225
|
});
|
|
146
226
|
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const testDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
let fixtureDirectory;
|
|
10
|
+
let sdkRoot;
|
|
11
|
+
let hmsSdkRoot;
|
|
12
|
+
let serverProcess;
|
|
13
|
+
let stdoutBuffer = Buffer.alloc(0);
|
|
14
|
+
const responses = [];
|
|
15
|
+
|
|
16
|
+
function encodeLspMessage(message) {
|
|
17
|
+
const body = Buffer.from(JSON.stringify(message));
|
|
18
|
+
return Buffer.concat([
|
|
19
|
+
Buffer.from(`Content-Length: ${body.length}\r\n\r\n`),
|
|
20
|
+
body,
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function collectLspMessages(chunk) {
|
|
25
|
+
stdoutBuffer = Buffer.concat([stdoutBuffer, chunk]);
|
|
26
|
+
|
|
27
|
+
while (true) {
|
|
28
|
+
const headerEnd = stdoutBuffer.indexOf('\r\n\r\n');
|
|
29
|
+
if (headerEnd === -1) return;
|
|
30
|
+
|
|
31
|
+
const header = stdoutBuffer.subarray(0, headerEnd).toString('ascii');
|
|
32
|
+
const lengthMatch = header.match(/Content-Length:\s*(\d+)/i);
|
|
33
|
+
if (!lengthMatch) return;
|
|
34
|
+
|
|
35
|
+
const bodyStart = headerEnd + 4;
|
|
36
|
+
const bodyLength = Number(lengthMatch[1]);
|
|
37
|
+
if (stdoutBuffer.length < bodyStart + bodyLength) return;
|
|
38
|
+
|
|
39
|
+
const body = stdoutBuffer.subarray(bodyStart, bodyStart + bodyLength);
|
|
40
|
+
responses.push(JSON.parse(body.toString('utf8')));
|
|
41
|
+
stdoutBuffer = stdoutBuffer.subarray(bodyStart + bodyLength);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function waitForResponse(predicate, timeout = 3000) {
|
|
46
|
+
const deadline = Date.now() + timeout;
|
|
47
|
+
while (Date.now() < deadline) {
|
|
48
|
+
const response = responses.find(predicate);
|
|
49
|
+
if (response) return response;
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
51
|
+
}
|
|
52
|
+
throw new Error('Timed out waiting for LSP response');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
beforeAll(async () => {
|
|
56
|
+
fixtureDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'zed-arkts-deveco-'));
|
|
57
|
+
sdkRoot = path.join(fixtureDirectory, 'Contents', 'sdk', 'default', 'openharmony');
|
|
58
|
+
await fs.mkdir(path.join(sdkRoot, 'ets', 'api'), { recursive: true });
|
|
59
|
+
await fs.mkdir(path.join(sdkRoot, 'ets', 'kits'), { recursive: true });
|
|
60
|
+
await fs.mkdir(path.join(sdkRoot, 'ets', 'component'), { recursive: true });
|
|
61
|
+
await fs.mkdir(path.join(sdkRoot, 'ets', 'build-tools', 'ets-loader', 'declarations'), {
|
|
62
|
+
recursive: true,
|
|
63
|
+
});
|
|
64
|
+
await fs.writeFile(
|
|
65
|
+
path.join(sdkRoot, 'ets', 'build-tools', 'ets-loader', 'tsconfig.json'),
|
|
66
|
+
'{}\n',
|
|
67
|
+
);
|
|
68
|
+
await fs.writeFile(
|
|
69
|
+
path.join(sdkRoot, 'ets', 'kits', '@kit.AbilityKit.d.ts'),
|
|
70
|
+
'export declare class UIAbility {}\n',
|
|
71
|
+
);
|
|
72
|
+
hmsSdkRoot = path.join(fixtureDirectory, 'Contents', 'sdk', 'default', 'hms');
|
|
73
|
+
await fs.mkdir(path.join(hmsSdkRoot, 'ets', 'api'), { recursive: true });
|
|
74
|
+
await fs.mkdir(path.join(hmsSdkRoot, 'ets', 'kits'), { recursive: true });
|
|
75
|
+
await fs.writeFile(
|
|
76
|
+
path.join(hmsSdkRoot, 'ets', 'kits', '@kit.TestHmsKit.d.ts'),
|
|
77
|
+
'export declare const hmsApi: string;\n',
|
|
78
|
+
);
|
|
79
|
+
const placeholderSiblingHms = path.join(fixtureDirectory, 'hms');
|
|
80
|
+
await fs.mkdir(path.join(placeholderSiblingHms, 'ets', 'api'), { recursive: true });
|
|
81
|
+
await fs.mkdir(path.join(placeholderSiblingHms, 'ets', 'kits'), { recursive: true });
|
|
82
|
+
|
|
83
|
+
const wrapperPath = path.join(testDirectory, '..', '..', 'index.js');
|
|
84
|
+
const mockServerPath = path.join(testDirectory, '..', 'mocks', 'mock-ets-server.js');
|
|
85
|
+
const tsdk = path.join(testDirectory, '..', '..', 'node_modules', 'ohos-typescript', 'lib');
|
|
86
|
+
const env = { ...process.env };
|
|
87
|
+
delete env.ZED_ETS_OHOS_SDK_PATH;
|
|
88
|
+
delete env.OHOS_SDK_PATH;
|
|
89
|
+
delete env.HARMONYOS_SDK_HOME;
|
|
90
|
+
delete env.OPENHARMONY_SDK_HOME;
|
|
91
|
+
delete env.DEVECO_SDK_HOME;
|
|
92
|
+
delete env.ZED_ETS_HMS_SDK_PATH;
|
|
93
|
+
delete env.HMS_SDK_PATH;
|
|
94
|
+
Object.assign(env, {
|
|
95
|
+
ETS_LANG_SERVER: mockServerPath,
|
|
96
|
+
ZED_ETS_TSDK: tsdk,
|
|
97
|
+
DEVECO_STUDIO_HOME: fixtureDirectory,
|
|
98
|
+
TMPDIR: fixtureDirectory,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
serverProcess = spawn(process.execPath, [wrapperPath], {
|
|
102
|
+
env,
|
|
103
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
104
|
+
});
|
|
105
|
+
serverProcess.stdout.on('data', collectLspMessages);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
afterAll(async () => {
|
|
109
|
+
serverProcess?.kill();
|
|
110
|
+
await fs.rm(fixtureDirectory, { recursive: true, force: true });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('SDK initialization', () => {
|
|
114
|
+
it('injects the DevEco OpenHarmony SDK into ArkTS initialization', async () => {
|
|
115
|
+
serverProcess.stdin.write(
|
|
116
|
+
encodeLspMessage({
|
|
117
|
+
jsonrpc: '2.0',
|
|
118
|
+
id: 1,
|
|
119
|
+
method: 'initialize',
|
|
120
|
+
params: {
|
|
121
|
+
processId: process.pid,
|
|
122
|
+
rootUri: 'file:///tmp/harmony-project',
|
|
123
|
+
capabilities: {},
|
|
124
|
+
initializationOptions: {},
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const response = await waitForResponse((message) => message.id === 1);
|
|
130
|
+
const initializationOptions = response.result.initializationOptions;
|
|
131
|
+
|
|
132
|
+
expect(initializationOptions.ohos.sdkPath).toBe(sdkRoot);
|
|
133
|
+
expect(initializationOptions.ets.sdkPath).toBe(sdkRoot);
|
|
134
|
+
expect(initializationOptions.ohos.baseUrl).toBe(path.join(sdkRoot, 'ets'));
|
|
135
|
+
expect(initializationOptions.ets.hmsPath).toBe(hmsSdkRoot);
|
|
136
|
+
expect(initializationOptions.ohos.hmsSdkPath).toBe(hmsSdkRoot);
|
|
137
|
+
expect(initializationOptions.ohos.paths['*']).toContain(
|
|
138
|
+
path.join(hmsSdkRoot, 'ets', 'kits', '*'),
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('does not infer an HMS SDK from the placeholder OpenHarmony SDK', async () => {
|
|
143
|
+
serverProcess.stdin.write(
|
|
144
|
+
encodeLspMessage({
|
|
145
|
+
jsonrpc: '2.0',
|
|
146
|
+
id: 2,
|
|
147
|
+
method: 'initialize',
|
|
148
|
+
params: {
|
|
149
|
+
processId: process.pid,
|
|
150
|
+
rootUri: 'file:///tmp/harmony-project',
|
|
151
|
+
capabilities: {},
|
|
152
|
+
initializationOptions: {
|
|
153
|
+
ohosSdkPath: path.join(fixtureDirectory, 'invalid-openharmony'),
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const response = await waitForResponse((message) => message.id === 2);
|
|
160
|
+
const initializationOptions = response.result.initializationOptions;
|
|
161
|
+
|
|
162
|
+
expect(initializationOptions.ets.sdkPath).toBe(
|
|
163
|
+
path.join(fixtureDirectory, 'zed-ets-empty-ohos-sdk'),
|
|
164
|
+
);
|
|
165
|
+
expect(initializationOptions.ets.hmsPath).toBeUndefined();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -91,10 +91,12 @@ struct Test {
|
|
|
91
91
|
textDocumentSync: 1,
|
|
92
92
|
documentFormattingProvider: true,
|
|
93
93
|
documentRangeFormattingProvider: true
|
|
94
|
-
}
|
|
94
|
+
},
|
|
95
|
+
// Echo what the wrapper forwarded so tests can assert on it.
|
|
96
|
+
initializationOptions: message.params?.initializationOptions ?? {}
|
|
95
97
|
}
|
|
96
98
|
};
|
|
97
|
-
|
|
99
|
+
|
|
98
100
|
process.send(response);
|
|
99
101
|
}
|
|
100
102
|
|