zed-ets-language-server 2.2.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.
- package/lib/data-parser.js +14 -13
- package/lib/data-parser.test.js +143 -0
- package/package.json +1 -1
package/lib/data-parser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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}
|
|
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
|
}
|
package/lib/data-parser.test.js
CHANGED
|
@@ -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', () => {
|