zed-ets-language-server 2.3.4 → 3.0.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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { spawn } from 'node:child_process';
4
4
  import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
5
7
  import { logger } from './lib/logger.js';
6
8
  import { parse } from './lib/data-parser.js';
7
9
  import { listHelperPaths } from './lib/lib-expander.js'
@@ -9,6 +11,40 @@ import { listHelperPaths } from './lib/lib-expander.js'
9
11
  // ETS language server path, passed by Rust extension process through environment variable
10
12
  const etsLangServerPath = process.env.ETS_LANG_SERVER;
11
13
 
14
+ // The extension installs ohos-typescript next to @arkts/language-server, so a
15
+ // usable tsdk can be derived from the server path when settings don't name one:
16
+ // <work dir>/node_modules/@arkts/language-server/bin/ets-language-server.js
17
+ // <work dir>/node_modules/ohos-typescript/lib
18
+ // @arkts/language-server v1.3+ refuses to initialize unless ets.sdkPath points at
19
+ // a directory containing ets/build-tools/ets-loader/tsconfig.json (v1.2 accepted
20
+ // any value). Provide a minimal skeleton so the server starts without a real SDK;
21
+ // ArkUI typings degrade but TypeScript-level features keep working.
22
+ function ensurePlaceholderSdk() {
23
+ const sdkDir = path.join(os.tmpdir(), 'zed-ets-empty-ohos-sdk');
24
+ const etsLoaderDir = path.join(sdkDir, 'ets', 'build-tools', 'ets-loader');
25
+ try {
26
+ fs.mkdirSync(path.join(etsLoaderDir, 'declarations'), { recursive: true });
27
+ fs.mkdirSync(path.join(sdkDir, 'ets', 'component'), { recursive: true });
28
+ const tsconfigPath = path.join(etsLoaderDir, 'tsconfig.json');
29
+ if (!fs.existsSync(tsconfigPath)) {
30
+ fs.writeFileSync(tsconfigPath, '{}\n');
31
+ }
32
+ } catch (error) {
33
+ logger.error(`Failed to prepare placeholder SDK dir ${sdkDir}: ${error.message}`);
34
+ }
35
+ return sdkDir;
36
+ }
37
+
38
+ function detectTsdk() {
39
+ if (!etsLangServerPath) return undefined;
40
+ const serverBinDir = path.dirname(etsLangServerPath);
41
+ const candidates = [
42
+ path.join(serverBinDir, '..', '..', '..', 'ohos-typescript', 'lib'),
43
+ path.join(serverBinDir, '..', 'node_modules', 'ohos-typescript', 'lib'),
44
+ ];
45
+ return candidates.find((dir) => fs.existsSync(path.join(dir, 'typescript.js')));
46
+ }
47
+
12
48
  async function main() {
13
49
  logger.section('🚀 ETS Language Server Wrapper');
14
50
 
@@ -39,21 +75,56 @@ async function main() {
39
75
  logger.info(`Language server process exited, exit code: ${code}, signal: ${signal}`);
40
76
  });
41
77
 
78
+ // Requests injected by this wrapper (not sent by the editor); their responses
79
+ // must not be forwarded to the editor, which never issued them.
80
+ const wrapperRequestIds = new Set();
81
+
42
82
  // Set up forwarding of serverProcess IPC messages to process.stdout
43
83
  serverProcess.on('message', (message) => {
84
+ if (message?.id !== undefined && wrapperRequestIds.delete(message.id)) {
85
+ logger.info(`Swallowed response to wrapper-injected request ${message.id}: ${JSON.stringify(message.result ?? message.error)}`);
86
+ return;
87
+ }
44
88
  // Convert IPC message to standard LSP format and send to stdout
45
89
  const messageStr = JSON.stringify(message);
46
90
  const headers = `Content-Length: ${Buffer.byteLength(messageStr)}\r\n\r\n`;
47
91
  process.stdout.write(headers + messageStr);
48
92
  });
49
93
 
50
- // Set up forwarding of process.stdin to serverProcess IPC
51
- process.stdin.setEncoding('utf8');
94
+ // Set up forwarding of process.stdin to serverProcess IPC.
95
+ // No setEncoding here: the parser needs raw bytes because LSP Content-Length
96
+ // counts bytes, not characters.
52
97
  process.stdin.on('data', (data) => parse(data, async (message) => {
53
98
  // This special ets request is required in document: https://github.com/ohosvscode/arkTS/tree/next/packages/language-server
54
99
  // When this goes wrong, ETS UI decorators and functions will be type of any
55
100
  if (message.method === 'initialize') {
56
- const { initializationOptions } = message.params;
101
+ message.params = message.params ?? {};
102
+ const initializationOptions = message.params.initializationOptions ?? {};
103
+ message.params.initializationOptions = initializationOptions;
104
+
105
+ // Zed only passes initializationOptions when the user configured
106
+ // lsp.arkts-language-server.initialization_options in settings. Fall back to
107
+ // env vars, then auto-detection, so the server starts out of the box.
108
+ if (!initializationOptions.tsdk) {
109
+ initializationOptions.tsdk = process.env.ZED_ETS_TSDK || process.env.TSDK || detectTsdk();
110
+ logger.info(`No tsdk in initializationOptions; falling back to: ${initializationOptions.tsdk}`);
111
+ }
112
+ if (!initializationOptions.ohosSdkPath) {
113
+ initializationOptions.ohosSdkPath = process.env.ZED_ETS_OHOS_SDK_PATH || process.env.OHOS_SDK_PATH;
114
+ }
115
+
116
+ // The server cannot finish `initialize` without a tsdk (it fails loading
117
+ // TypeScript and Zed reports "Failed to start language server").
118
+ if (!initializationOptions.tsdk) {
119
+ logger.error(`No tsdk in LSP settings, env (ZED_ETS_TSDK/TSDK), or next to ${etsLangServerPath}; forwarding initialize as-is, the server will likely fail to start.`);
120
+ serverProcess.send(message);
121
+ return;
122
+ }
123
+
124
+ if (!initializationOptions.ohosSdkPath) {
125
+ initializationOptions.ohosSdkPath = ensurePlaceholderSdk();
126
+ logger.error('No ohosSdkPath in LSP settings or env (ZED_ETS_OHOS_SDK_PATH/OHOS_SDK_PATH); using a placeholder SDK skeleton, ArkUI SDK types will be unavailable until lsp.arkts-language-server.initialization_options.ohosSdkPath is set in Zed settings.');
127
+ }
57
128
 
58
129
  const ohos = await listHelperPaths(initializationOptions.tsdk, initializationOptions.ohosSdkPath);
59
130
 
@@ -61,7 +132,7 @@ async function main() {
61
132
  // @arkts/language-server v1.2.x (uses `ohos`) and v1.3.x+ (uses `ets`).
62
133
  const etsSpecialRequest = {
63
134
  jsonrpc: '2.0',
64
- id: Date.now(),
135
+ id: `zed-ets-wrapper-${Date.now()}`,
65
136
  method: 'ets/waitForEtsConfigurationChangedRequested',
66
137
  params: {
67
138
  typescript: {
@@ -81,7 +152,8 @@ async function main() {
81
152
 
82
153
  logger.info(JSON.stringify(generalInitRequest));
83
154
  logger.info(JSON.stringify(etsSpecialRequest));
84
-
155
+
156
+ wrapperRequestIds.add(etsSpecialRequest.id);
85
157
  serverProcess.send(generalInitRequest);
86
158
  serverProcess.send(etsSpecialRequest);
87
159
  return;
@@ -1,38 +1,43 @@
1
1
  import { logger } from './logger.js';
2
2
 
3
- let stdinBuffer = '';
3
+ let stdinBuffer = Buffer.alloc(0);
4
4
 
5
5
  export function parse(data, callback) {
6
- stdinBuffer += data.toString();
6
+ // LSP Content-Length is measured in bytes, not characters, so all parsing
7
+ // must operate on a Buffer. Convert string input to a Buffer if necessary
8
+ // (happens when stdin.setEncoding('utf8') is used, which yields strings).
9
+ const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
10
+ stdinBuffer = Buffer.concat([stdinBuffer, dataBuffer]);
7
11
 
8
12
  while (true) {
9
- // Find Content-Length header
10
- const lengthMatch = stdinBuffer.match(/Content-Length: (\d+)\r\n/);
11
- if (!lengthMatch) break;
12
-
13
- const contentLength = Number.parseInt(lengthMatch[1]);
14
13
  const headerEnd = stdinBuffer.indexOf('\r\n\r\n');
15
-
16
14
  if (headerEnd === -1) break;
17
15
 
16
+ const headerPart = stdinBuffer.subarray(0, headerEnd).toString('utf8');
17
+ const lengthMatch = headerPart.match(/Content-Length: (\d+)/);
18
+ if (!lengthMatch) break;
19
+
20
+ const contentLength = Number.parseInt(lengthMatch[1]);
18
21
  const messageStart = headerEnd + 4;
19
22
  const messageEnd = messageStart + contentLength;
20
23
 
24
+ // Content-Length counts bytes, so compare against the buffer's byte length.
21
25
  if (stdinBuffer.length < messageEnd) break;
22
26
 
23
- // Extract message
24
- const messageJson = stdinBuffer.substring(messageStart, messageEnd);
25
- stdinBuffer = stdinBuffer.substring(messageEnd);
27
+ const messageJson = stdinBuffer.subarray(messageStart, messageEnd).toString('utf8');
28
+ stdinBuffer = stdinBuffer.subarray(messageEnd);
26
29
 
27
30
  try {
28
31
  const message = JSON.parse(messageJson);
29
32
  callback(message);
30
33
  } catch (error) {
31
34
  logger.error(`Error parsing message: ${error.message} ${error.stack} ${messageJson}`);
35
+ // Clear buffer on parse error to prevent corruption from leftover data
36
+ stdinBuffer = Buffer.alloc(0);
32
37
  }
33
38
  }
34
39
  }
35
40
 
36
41
  export function clearBuffer() {
37
- stdinBuffer = '';
42
+ stdinBuffer = Buffer.alloc(0);
38
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zed-ets-language-server",
3
- "version": "2.3.4",
3
+ "version": "3.0.0",
4
4
  "description": "ETS language server wrapper for Zed ArkTS extension.",
5
5
  "type": "module",
6
6
  "main": "index.js",