zed-ets-language-server 2.3.0 → 2.3.1

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.
@@ -1,38 +1,39 @@
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
+ // Convert string to Buffer if necessary (happens when stdin.setEncoding('utf8') is used)
7
+ const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
8
+ stdinBuffer = Buffer.concat([stdinBuffer, dataBuffer]);
7
9
 
8
10
  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
11
  const headerEnd = stdinBuffer.indexOf('\r\n\r\n');
15
-
16
12
  if (headerEnd === -1) break;
17
13
 
14
+ const headerPart = stdinBuffer.subarray(0, headerEnd).toString('utf8');
15
+ const lengthMatch = headerPart.match(/Content-Length: (\d+)/);
16
+ if (!lengthMatch) break;
17
+
18
+ const contentLength = Number.parseInt(lengthMatch[1]);
18
19
  const messageStart = headerEnd + 4;
19
20
  const messageEnd = messageStart + contentLength;
20
21
 
21
22
  if (stdinBuffer.length < messageEnd) break;
22
23
 
23
- // Extract message
24
- const messageJson = stdinBuffer.substring(messageStart, messageEnd);
25
- stdinBuffer = stdinBuffer.substring(messageEnd);
24
+ const messageJson = stdinBuffer.subarray(messageStart, messageEnd).toString('utf8');
25
+ stdinBuffer = stdinBuffer.subarray(messageEnd);
26
26
 
27
27
  try {
28
28
  const message = JSON.parse(messageJson);
29
29
  callback(message);
30
30
  } catch (error) {
31
- logger.error(`Error parsing message: ${error.message} ${error.stack} ${messageJson}`);
31
+ logger.error(`Error parsing message: ${error.message}`);
32
+ stdinBuffer = Buffer.alloc(0);
32
33
  }
33
34
  }
34
35
  }
35
36
 
36
37
  export function clearBuffer() {
37
- stdinBuffer = '';
38
+ stdinBuffer = Buffer.alloc(0);
38
39
  }
@@ -233,6 +233,149 @@ describe('data-parser', () => {
233
233
 
234
234
  parse(data, callback);
235
235
  });
236
+
237
+ describe('UTF-8 byte length handling', () => {
238
+ it('should correctly parse messages with Chinese characters (multi-byte UTF-8)', (done) => {
239
+ // This test specifically targets the bug where Content-Length (bytes)
240
+ // was incorrectly treated as character count
241
+ const message = {
242
+ jsonrpc: '2.0',
243
+ method: 'textDocument/didOpen',
244
+ params: {
245
+ textDocument: {
246
+ uri: 'file:///test.ets',
247
+ languageId: 'arkts',
248
+ text: '// 简单的ETS示例 - 基础语法演示\nconst APP_NAME: string = \'ETS应用\';'
249
+ }
250
+ }
251
+ };
252
+
253
+ const messageJson = JSON.stringify(message);
254
+ // Content-Length must be byte length, not character length
255
+ const byteLength = Buffer.byteLength(messageJson, 'utf8');
256
+ const data = Buffer.from(`Content-Length: ${byteLength}\r\n\r\n${messageJson}`);
257
+
258
+ const callback = vi.fn((parsedMessage) => {
259
+ expect(parsedMessage).toEqual(message);
260
+ expect(callback).toHaveBeenCalledTimes(1);
261
+ done();
262
+ });
263
+
264
+ parse(data, callback);
265
+ });
266
+
267
+ it('should handle message where char length != byte length followed by another message', () => {
268
+ // Regression test: multiple messages with Chinese content
269
+ // Previously, the second message header would be incorrectly included in the first
270
+ const message1 = {
271
+ jsonrpc: '2.0',
272
+ method: 'textDocument/didOpen',
273
+ params: {
274
+ textDocument: {
275
+ uri: 'file:///测试文件.ets',
276
+ text: 'const 中文变量 = "测试内容包含多字节字符";'
277
+ }
278
+ }
279
+ };
280
+ const message2 = {
281
+ jsonrpc: '2.0',
282
+ method: 'textDocument/publishDiagnostics',
283
+ params: {
284
+ uri: 'file:///测试文件.ets',
285
+ diagnostics: []
286
+ }
287
+ };
288
+
289
+ const messageJson1 = JSON.stringify(message1);
290
+ const messageJson2 = JSON.stringify(message2);
291
+ const byteLength1 = Buffer.byteLength(messageJson1, 'utf8');
292
+ const byteLength2 = Buffer.byteLength(messageJson2, 'utf8');
293
+
294
+ const data = Buffer.from(
295
+ `Content-Length: ${byteLength1}\r\n\r\n${messageJson1}` +
296
+ `Content-Length: ${byteLength2}\r\n\r\n${messageJson2}`
297
+ );
298
+
299
+ const callback = vi.fn();
300
+ parse(data, callback);
301
+
302
+ expect(callback).toHaveBeenCalledTimes(2);
303
+ expect(callback).toHaveBeenNthCalledWith(1, message1);
304
+ expect(callback).toHaveBeenNthCalledWith(2, message2);
305
+ });
306
+
307
+ it('should correctly calculate byte length for emoji characters', (done) => {
308
+ // Emojis are 4 bytes each in UTF-8
309
+ const message = {
310
+ jsonrpc: '2.0',
311
+ method: 'textDocument/didOpen',
312
+ params: {
313
+ textDocument: {
314
+ uri: 'file:///test.ets',
315
+ text: '🚀🎯💻🎉🎊' // 5 emojis = 20 bytes
316
+ }
317
+ }
318
+ };
319
+
320
+ const messageJson = JSON.stringify(message);
321
+ const byteLength = Buffer.byteLength(messageJson, 'utf8');
322
+ const data = Buffer.from(`Content-Length: ${byteLength}\r\n\r\n${messageJson}`);
323
+
324
+ const callback = vi.fn((parsedMessage) => {
325
+ expect(parsedMessage).toEqual(message);
326
+ done();
327
+ });
328
+
329
+ parse(data, callback);
330
+ });
331
+
332
+ it('should verify byte length differs from char length for non-ASCII', () => {
333
+ // This test documents why the Buffer-based fix was necessary
334
+ const text = '中文测试 🚀 émoji';
335
+ const charLength = text.length;
336
+ const byteLength = Buffer.byteLength(text, 'utf8');
337
+
338
+ // Verify that byte length > char length for non-ASCII
339
+ expect(byteLength).toBeGreaterThan(charLength);
340
+
341
+ // Create a message with this text
342
+ const message = { jsonrpc: '2.0', method: 'test', params: { text } };
343
+ const messageJson = JSON.stringify(message);
344
+ const data = Buffer.from(`Content-Length: ${Buffer.byteLength(messageJson, 'utf8')}\r\n\r\n${messageJson}`);
345
+
346
+ const callback = vi.fn();
347
+ parse(data, callback);
348
+
349
+ expect(callback).toHaveBeenCalledWith(message);
350
+ });
351
+
352
+ it('should handle partial message split at multi-byte character boundary', (done) => {
353
+ // Simulate TCP fragmentation that splits in the middle of a multi-byte character
354
+ const message = {
355
+ jsonrpc: '2.0',
356
+ method: 'test',
357
+ params: { text: '测试中文字符' }
358
+ };
359
+ const messageJson = JSON.stringify(message);
360
+ const byteLength = Buffer.byteLength(messageJson, 'utf8');
361
+ const fullData = Buffer.from(`Content-Length: ${byteLength}\r\n\r\n${messageJson}`);
362
+
363
+ // Split at a position that cuts through a multi-byte character
364
+ const splitPoint = fullData.indexOf('测试') + 3; // Middle of '测' or '试'
365
+ const part1 = fullData.subarray(0, splitPoint);
366
+ const part2 = fullData.subarray(splitPoint);
367
+
368
+ const callback = vi.fn((parsedMessage) => {
369
+ expect(parsedMessage).toEqual(message);
370
+ done();
371
+ });
372
+
373
+ parse(part1, callback);
374
+ expect(callback).not.toHaveBeenCalled();
375
+
376
+ parse(part2, callback);
377
+ });
378
+ });
236
379
  });
237
380
 
238
381
  describe('clearBuffer', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zed-ets-language-server",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "ETS language server wrapper for Zed ArkTS extension.",
5
5
  "type": "module",
6
6
  "main": "index.js",