partnercore-proxy 0.1.5 → 0.4.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/CHANGELOG.md +114 -6
- package/README.md +168 -130
- package/dist/al/extension-manager.js +10 -50
- package/dist/al/extension-manager.js.map +1 -1
- package/dist/al/index.js +2 -18
- package/dist/al/index.js.map +1 -1
- package/dist/al/language-server.d.ts +315 -2
- package/dist/al/language-server.d.ts.map +1 -1
- package/dist/al/language-server.js +685 -47
- package/dist/al/language-server.js.map +1 -1
- package/dist/cli.js +36 -68
- package/dist/cli.js.map +1 -1
- package/dist/cloud/index.js +1 -17
- package/dist/cloud/index.js.map +1 -1
- package/dist/cloud/relay-client.js +5 -12
- package/dist/cloud/relay-client.js.map +1 -1
- package/dist/config/index.js +2 -18
- package/dist/config/index.js.map +1 -1
- package/dist/config/loader.js +8 -47
- package/dist/config/loader.js.map +1 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +65 -4
- package/dist/config/types.js.map +1 -1
- package/dist/container/bc-container.d.ts +212 -0
- package/dist/container/bc-container.d.ts.map +1 -0
- package/dist/container/bc-container.js +703 -0
- package/dist/container/bc-container.js.map +1 -0
- package/dist/git/git-operations.d.ts +182 -0
- package/dist/git/git-operations.d.ts.map +1 -0
- package/dist/git/git-operations.js +442 -0
- package/dist/git/git-operations.js.map +1 -0
- package/dist/index.js +6 -22
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +1 -17
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server.js +10 -14
- package/dist/mcp/server.js.map +1 -1
- package/dist/memory/project-memory.d.ts +83 -0
- package/dist/memory/project-memory.d.ts.map +1 -0
- package/dist/memory/project-memory.js +273 -0
- package/dist/memory/project-memory.js.map +1 -0
- package/dist/router/index.js +1 -17
- package/dist/router/index.js.map +1 -1
- package/dist/router/tool-router.d.ts +62 -0
- package/dist/router/tool-router.d.ts.map +1 -1
- package/dist/router/tool-router.js +2577 -328
- package/dist/router/tool-router.js.map +1 -1
- package/dist/utils/index.js +2 -18
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.js +8 -16
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/security.js +10 -54
- package/dist/utils/security.js.map +1 -1
- package/package.json +4 -3
|
@@ -1,59 +1,23 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* AL Language Server Client
|
|
4
3
|
*
|
|
5
4
|
* Communicates with the Microsoft AL Language Server via LSP protocol.
|
|
6
|
-
*
|
|
5
|
+
* Provides full LSP integration for AL development.
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
Object.defineProperty(o, k2, desc);
|
|
15
|
-
}) : (function(o, m, k, k2) {
|
|
16
|
-
if (k2 === undefined) k2 = k;
|
|
17
|
-
o[k2] = m[k];
|
|
18
|
-
}));
|
|
19
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
-
}) : function(o, v) {
|
|
22
|
-
o["default"] = v;
|
|
23
|
-
});
|
|
24
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
-
var ownKeys = function(o) {
|
|
26
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
-
var ar = [];
|
|
28
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
-
return ar;
|
|
30
|
-
};
|
|
31
|
-
return ownKeys(o);
|
|
32
|
-
};
|
|
33
|
-
return function (mod) {
|
|
34
|
-
if (mod && mod.__esModule) return mod;
|
|
35
|
-
var result = {};
|
|
36
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
-
__setModuleDefault(result, mod);
|
|
38
|
-
return result;
|
|
39
|
-
};
|
|
40
|
-
})();
|
|
41
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
-
exports.ALLanguageServer = void 0;
|
|
43
|
-
const child_process_1 = require("child_process");
|
|
44
|
-
const path = __importStar(require("path"));
|
|
45
|
-
const fs = __importStar(require("fs"));
|
|
46
|
-
const node_js_1 = require("vscode-jsonrpc/node.js");
|
|
47
|
-
const logger_js_1 = require("../utils/logger.js");
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import { createMessageConnection, StreamMessageReader, StreamMessageWriter, } from 'vscode-jsonrpc/node.js';
|
|
11
|
+
import { getLogger } from '../utils/logger.js';
|
|
48
12
|
/**
|
|
49
13
|
* AL Language Server Client
|
|
50
14
|
*/
|
|
51
|
-
class ALLanguageServer {
|
|
15
|
+
export class ALLanguageServer {
|
|
52
16
|
extensionInfo;
|
|
53
17
|
workspaceRoot;
|
|
54
18
|
process = null;
|
|
55
19
|
connection = null;
|
|
56
|
-
logger =
|
|
20
|
+
logger = getLogger();
|
|
57
21
|
initialized = false;
|
|
58
22
|
openDocuments = new Set();
|
|
59
23
|
diagnosticsCache = new Map();
|
|
@@ -70,7 +34,7 @@ class ALLanguageServer {
|
|
|
70
34
|
}
|
|
71
35
|
this.logger.info('Starting AL Language Server...');
|
|
72
36
|
// Start the EditorServices process
|
|
73
|
-
this.process =
|
|
37
|
+
this.process = spawn(this.extensionInfo.editorServicesPath, [], {
|
|
74
38
|
cwd: this.workspaceRoot,
|
|
75
39
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
76
40
|
});
|
|
@@ -78,7 +42,7 @@ class ALLanguageServer {
|
|
|
78
42
|
throw new Error('Failed to start AL Language Server process');
|
|
79
43
|
}
|
|
80
44
|
// Create LSP connection
|
|
81
|
-
this.connection =
|
|
45
|
+
this.connection = createMessageConnection(new StreamMessageReader(this.process.stdout), new StreamMessageWriter(this.process.stdin));
|
|
82
46
|
// Handle diagnostics
|
|
83
47
|
this.connection.onNotification('textDocument/publishDiagnostics', (params) => {
|
|
84
48
|
this.handleDiagnostics(params);
|
|
@@ -146,6 +110,91 @@ class ALLanguageServer {
|
|
|
146
110
|
publishDiagnostics: {
|
|
147
111
|
relatedInformation: true,
|
|
148
112
|
},
|
|
113
|
+
codeAction: {
|
|
114
|
+
dynamicRegistration: true,
|
|
115
|
+
codeActionLiteralSupport: {
|
|
116
|
+
codeActionKind: {
|
|
117
|
+
valueSet: [
|
|
118
|
+
'quickfix',
|
|
119
|
+
'refactor',
|
|
120
|
+
'refactor.extract',
|
|
121
|
+
'refactor.inline',
|
|
122
|
+
'refactor.rewrite',
|
|
123
|
+
'source',
|
|
124
|
+
'source.organizeImports',
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
resolveSupport: {
|
|
129
|
+
properties: ['edit'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
signatureHelp: {
|
|
133
|
+
dynamicRegistration: true,
|
|
134
|
+
signatureInformation: {
|
|
135
|
+
documentationFormat: ['markdown', 'plaintext'],
|
|
136
|
+
parameterInformation: {
|
|
137
|
+
labelOffsetSupport: true,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
contextSupport: true,
|
|
141
|
+
},
|
|
142
|
+
formatting: {
|
|
143
|
+
dynamicRegistration: true,
|
|
144
|
+
},
|
|
145
|
+
rangeFormatting: {
|
|
146
|
+
dynamicRegistration: true,
|
|
147
|
+
},
|
|
148
|
+
onTypeFormatting: {
|
|
149
|
+
dynamicRegistration: true,
|
|
150
|
+
},
|
|
151
|
+
documentHighlight: {
|
|
152
|
+
dynamicRegistration: true,
|
|
153
|
+
},
|
|
154
|
+
foldingRange: {
|
|
155
|
+
dynamicRegistration: true,
|
|
156
|
+
foldingRangeKind: {
|
|
157
|
+
valueSet: ['comment', 'imports', 'region'],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
selectionRange: {
|
|
161
|
+
dynamicRegistration: true,
|
|
162
|
+
},
|
|
163
|
+
codeLens: {
|
|
164
|
+
dynamicRegistration: true,
|
|
165
|
+
},
|
|
166
|
+
documentLink: {
|
|
167
|
+
dynamicRegistration: true,
|
|
168
|
+
tooltipSupport: true,
|
|
169
|
+
},
|
|
170
|
+
typeDefinition: {
|
|
171
|
+
dynamicRegistration: true,
|
|
172
|
+
linkSupport: true,
|
|
173
|
+
},
|
|
174
|
+
implementation: {
|
|
175
|
+
dynamicRegistration: true,
|
|
176
|
+
linkSupport: true,
|
|
177
|
+
},
|
|
178
|
+
semanticTokens: {
|
|
179
|
+
dynamicRegistration: true,
|
|
180
|
+
tokenTypes: [
|
|
181
|
+
'namespace', 'type', 'class', 'enum', 'interface', 'struct',
|
|
182
|
+
'typeParameter', 'parameter', 'variable', 'property', 'enumMember',
|
|
183
|
+
'event', 'function', 'method', 'macro', 'keyword', 'modifier',
|
|
184
|
+
'comment', 'string', 'number', 'regexp', 'operator', 'decorator',
|
|
185
|
+
],
|
|
186
|
+
tokenModifiers: [
|
|
187
|
+
'declaration', 'definition', 'readonly', 'static', 'deprecated',
|
|
188
|
+
'abstract', 'async', 'modification', 'documentation', 'defaultLibrary',
|
|
189
|
+
],
|
|
190
|
+
formats: ['relative'],
|
|
191
|
+
requests: {
|
|
192
|
+
full: true,
|
|
193
|
+
range: true,
|
|
194
|
+
},
|
|
195
|
+
multilineTokenSupport: false,
|
|
196
|
+
overlappingTokenSupport: false,
|
|
197
|
+
},
|
|
149
198
|
},
|
|
150
199
|
workspace: {
|
|
151
200
|
applyEdit: true,
|
|
@@ -313,6 +362,269 @@ class ALLanguageServer {
|
|
|
313
362
|
const result = await this.connection.sendRequest('workspace/symbol', { query });
|
|
314
363
|
return result || [];
|
|
315
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Update document content (for editing)
|
|
367
|
+
*/
|
|
368
|
+
async updateDocument(uri, content, version) {
|
|
369
|
+
if (!this.connection) {
|
|
370
|
+
throw new Error('Language server not initialized');
|
|
371
|
+
}
|
|
372
|
+
// If document is not open, open it first
|
|
373
|
+
if (!this.openDocuments.has(uri)) {
|
|
374
|
+
await this.openDocument(uri);
|
|
375
|
+
}
|
|
376
|
+
const params = {
|
|
377
|
+
textDocument: { uri, version },
|
|
378
|
+
contentChanges: [{ text: content }],
|
|
379
|
+
};
|
|
380
|
+
void this.connection.sendNotification('textDocument/didChange', params);
|
|
381
|
+
// Wait for diagnostics to be processed
|
|
382
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Rename symbol across the workspace
|
|
386
|
+
* @returns WorkspaceEdit with all changes needed
|
|
387
|
+
*/
|
|
388
|
+
async renameSymbol(uri, line, character, newName) {
|
|
389
|
+
await this.ensureInitialized();
|
|
390
|
+
await this.openDocument(uri);
|
|
391
|
+
// First, check if rename is valid
|
|
392
|
+
const prepareParams = {
|
|
393
|
+
textDocument: { uri },
|
|
394
|
+
position: { line, character },
|
|
395
|
+
};
|
|
396
|
+
try {
|
|
397
|
+
const prepareResult = await this.connection.sendRequest('textDocument/prepareRename', prepareParams);
|
|
398
|
+
if (!prepareResult) {
|
|
399
|
+
this.logger.debug('Rename not available at this position');
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
// Perform the rename
|
|
403
|
+
const renameParams = {
|
|
404
|
+
textDocument: { uri },
|
|
405
|
+
position: { line, character },
|
|
406
|
+
newName,
|
|
407
|
+
};
|
|
408
|
+
const result = await this.connection.sendRequest('textDocument/rename', renameParams);
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
this.logger.error('Rename failed:', error);
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Get the symbol at a specific position
|
|
418
|
+
*/
|
|
419
|
+
async getSymbolAtPosition(uri, line, character) {
|
|
420
|
+
const symbols = await this.getDocumentSymbols(uri);
|
|
421
|
+
return this.findSymbolAtPosition(symbols, line, character);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Get code actions (quick fixes, refactorings) at a specific position or range
|
|
425
|
+
*/
|
|
426
|
+
async getCodeActions(uri, range, context) {
|
|
427
|
+
await this.ensureInitialized();
|
|
428
|
+
await this.openDocument(uri);
|
|
429
|
+
// Build diagnostics with proper typing
|
|
430
|
+
const diagnostics = context?.diagnostics?.map(d => ({
|
|
431
|
+
message: d.message,
|
|
432
|
+
severity: this.severityToNumber(d.severity),
|
|
433
|
+
range: d.range,
|
|
434
|
+
code: d.code,
|
|
435
|
+
source: d.source,
|
|
436
|
+
})) || [];
|
|
437
|
+
const params = {
|
|
438
|
+
textDocument: { uri },
|
|
439
|
+
range,
|
|
440
|
+
context: {
|
|
441
|
+
diagnostics,
|
|
442
|
+
only: context?.only,
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
const result = await this.connection.sendRequest('textDocument/codeAction', params);
|
|
446
|
+
if (!result)
|
|
447
|
+
return [];
|
|
448
|
+
return result.map(item => {
|
|
449
|
+
// Check if it's a CodeAction (has 'title' as required field for both, but CodeAction has more fields)
|
|
450
|
+
const isCodeAction = 'kind' in item || 'edit' in item || ('command' in item && typeof item.command === 'object');
|
|
451
|
+
if (isCodeAction) {
|
|
452
|
+
const action = item;
|
|
453
|
+
let editResult;
|
|
454
|
+
if (action.edit && action.edit.changes) {
|
|
455
|
+
editResult = {
|
|
456
|
+
changes: action.edit.changes,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
title: action.title,
|
|
461
|
+
kind: action.kind,
|
|
462
|
+
isPreferred: action.isPreferred,
|
|
463
|
+
edit: editResult,
|
|
464
|
+
command: action.command ? {
|
|
465
|
+
title: action.command.title,
|
|
466
|
+
command: action.command.command,
|
|
467
|
+
arguments: action.command.arguments,
|
|
468
|
+
} : undefined,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
// It's a Command (simpler structure: title, command string, arguments)
|
|
473
|
+
const cmd = item;
|
|
474
|
+
return {
|
|
475
|
+
title: cmd.title,
|
|
476
|
+
command: {
|
|
477
|
+
title: cmd.title,
|
|
478
|
+
command: cmd.command,
|
|
479
|
+
arguments: cmd.arguments,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Get signature help (function parameter hints) at a position
|
|
487
|
+
*/
|
|
488
|
+
async getSignatureHelp(uri, line, character, context) {
|
|
489
|
+
await this.ensureInitialized();
|
|
490
|
+
await this.openDocument(uri);
|
|
491
|
+
const params = {
|
|
492
|
+
textDocument: { uri },
|
|
493
|
+
position: { line, character },
|
|
494
|
+
context: context ? {
|
|
495
|
+
triggerKind: context.triggerKind || 1,
|
|
496
|
+
triggerCharacter: context.triggerCharacter,
|
|
497
|
+
isRetrigger: context.isRetrigger || false,
|
|
498
|
+
} : undefined,
|
|
499
|
+
};
|
|
500
|
+
const result = await this.connection.sendRequest('textDocument/signatureHelp', params);
|
|
501
|
+
if (!result)
|
|
502
|
+
return null;
|
|
503
|
+
return {
|
|
504
|
+
signatures: result.signatures.map(sig => ({
|
|
505
|
+
label: sig.label,
|
|
506
|
+
documentation: typeof sig.documentation === 'string'
|
|
507
|
+
? sig.documentation
|
|
508
|
+
: sig.documentation?.value,
|
|
509
|
+
parameters: sig.parameters?.map(p => ({
|
|
510
|
+
label: p.label,
|
|
511
|
+
documentation: typeof p.documentation === 'string'
|
|
512
|
+
? p.documentation
|
|
513
|
+
: p.documentation?.value,
|
|
514
|
+
})),
|
|
515
|
+
})),
|
|
516
|
+
activeSignature: result.activeSignature,
|
|
517
|
+
activeParameter: result.activeParameter,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Format an entire document
|
|
522
|
+
*/
|
|
523
|
+
async formatDocument(uri, options) {
|
|
524
|
+
await this.ensureInitialized();
|
|
525
|
+
await this.openDocument(uri);
|
|
526
|
+
const formattingOptions = {
|
|
527
|
+
tabSize: options?.tabSize ?? 4,
|
|
528
|
+
insertSpaces: options?.insertSpaces ?? true,
|
|
529
|
+
};
|
|
530
|
+
const params = {
|
|
531
|
+
textDocument: { uri },
|
|
532
|
+
options: formattingOptions,
|
|
533
|
+
};
|
|
534
|
+
const result = await this.connection.sendRequest('textDocument/formatting', params);
|
|
535
|
+
if (!result)
|
|
536
|
+
return [];
|
|
537
|
+
return result.map(edit => ({
|
|
538
|
+
range: {
|
|
539
|
+
start: { line: edit.range.start.line, character: edit.range.start.character },
|
|
540
|
+
end: { line: edit.range.end.line, character: edit.range.end.character },
|
|
541
|
+
},
|
|
542
|
+
newText: edit.newText,
|
|
543
|
+
}));
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Format a range within a document
|
|
547
|
+
*/
|
|
548
|
+
async formatRange(uri, range, options) {
|
|
549
|
+
await this.ensureInitialized();
|
|
550
|
+
await this.openDocument(uri);
|
|
551
|
+
const formattingOptions = {
|
|
552
|
+
tabSize: options?.tabSize ?? 4,
|
|
553
|
+
insertSpaces: options?.insertSpaces ?? true,
|
|
554
|
+
};
|
|
555
|
+
const params = {
|
|
556
|
+
textDocument: { uri },
|
|
557
|
+
range,
|
|
558
|
+
options: formattingOptions,
|
|
559
|
+
};
|
|
560
|
+
const result = await this.connection.sendRequest('textDocument/rangeFormatting', params);
|
|
561
|
+
if (!result)
|
|
562
|
+
return [];
|
|
563
|
+
return result.map(edit => ({
|
|
564
|
+
range: {
|
|
565
|
+
start: { line: edit.range.start.line, character: edit.range.start.character },
|
|
566
|
+
end: { line: edit.range.end.line, character: edit.range.end.character },
|
|
567
|
+
},
|
|
568
|
+
newText: edit.newText,
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Convert severity string to LSP severity number
|
|
573
|
+
*/
|
|
574
|
+
severityToNumber(severity) {
|
|
575
|
+
switch (severity) {
|
|
576
|
+
case 'error': return 1;
|
|
577
|
+
case 'warning': return 2;
|
|
578
|
+
case 'info': return 3;
|
|
579
|
+
case 'hint': return 4;
|
|
580
|
+
default: return 3;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Find symbol containing the given position
|
|
585
|
+
*/
|
|
586
|
+
findSymbolAtPosition(symbols, line, character) {
|
|
587
|
+
for (const symbol of symbols) {
|
|
588
|
+
const { start, end } = symbol.range;
|
|
589
|
+
// Check if position is within this symbol's range
|
|
590
|
+
const isAfterStart = line > start.line || (line === start.line && character >= start.character);
|
|
591
|
+
const isBeforeEnd = line < end.line || (line === end.line && character <= end.character);
|
|
592
|
+
if (isAfterStart && isBeforeEnd) {
|
|
593
|
+
// Check children first (more specific match)
|
|
594
|
+
if (symbol.children) {
|
|
595
|
+
const childMatch = this.findSymbolAtPosition(symbol.children, line, character);
|
|
596
|
+
if (childMatch) {
|
|
597
|
+
return childMatch;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return symbol;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Find a symbol by name in the document
|
|
607
|
+
*/
|
|
608
|
+
async findSymbolByName(uri, symbolName) {
|
|
609
|
+
const symbols = await this.getDocumentSymbols(uri);
|
|
610
|
+
return this.searchSymbolByName(symbols, symbolName);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Search for symbol by name recursively
|
|
614
|
+
*/
|
|
615
|
+
searchSymbolByName(symbols, name) {
|
|
616
|
+
for (const symbol of symbols) {
|
|
617
|
+
if (symbol.name.toLowerCase() === name.toLowerCase()) {
|
|
618
|
+
return symbol;
|
|
619
|
+
}
|
|
620
|
+
if (symbol.children) {
|
|
621
|
+
const found = this.searchSymbolByName(symbol.children, name);
|
|
622
|
+
if (found)
|
|
623
|
+
return found;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
316
628
|
/**
|
|
317
629
|
* Convert LSP symbols to our format
|
|
318
630
|
*/
|
|
@@ -403,6 +715,333 @@ class ALLanguageServer {
|
|
|
403
715
|
await this.initialize();
|
|
404
716
|
}
|
|
405
717
|
}
|
|
718
|
+
// ==================== Remaining LSP Features ====================
|
|
719
|
+
/**
|
|
720
|
+
* Close a document (cleanup)
|
|
721
|
+
*/
|
|
722
|
+
async closeDocument(uri) {
|
|
723
|
+
await this.ensureInitialized();
|
|
724
|
+
if (!this.openDocuments.has(uri)) {
|
|
725
|
+
return; // Not open
|
|
726
|
+
}
|
|
727
|
+
const params = {
|
|
728
|
+
textDocument: { uri },
|
|
729
|
+
};
|
|
730
|
+
await this.connection.sendNotification('textDocument/didClose', params);
|
|
731
|
+
this.openDocuments.delete(uri);
|
|
732
|
+
this.diagnosticsCache.delete(uri);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Notify save (triggers recompile in some LSPs)
|
|
736
|
+
*/
|
|
737
|
+
async saveDocument(uri, text) {
|
|
738
|
+
await this.ensureInitialized();
|
|
739
|
+
await this.openDocument(uri);
|
|
740
|
+
const params = {
|
|
741
|
+
textDocument: { uri },
|
|
742
|
+
text,
|
|
743
|
+
};
|
|
744
|
+
await this.connection.sendNotification('textDocument/didSave', params);
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Get document highlights (all occurrences of symbol under cursor)
|
|
748
|
+
*/
|
|
749
|
+
async getDocumentHighlights(uri, line, character) {
|
|
750
|
+
await this.ensureInitialized();
|
|
751
|
+
await this.openDocument(uri);
|
|
752
|
+
const params = {
|
|
753
|
+
textDocument: { uri },
|
|
754
|
+
position: { line, character },
|
|
755
|
+
};
|
|
756
|
+
const result = await this.connection.sendRequest('textDocument/documentHighlight', params);
|
|
757
|
+
if (!result)
|
|
758
|
+
return [];
|
|
759
|
+
return result.map(h => ({
|
|
760
|
+
range: {
|
|
761
|
+
start: { line: h.range.start.line, character: h.range.start.character },
|
|
762
|
+
end: { line: h.range.end.line, character: h.range.end.character },
|
|
763
|
+
},
|
|
764
|
+
kind: h.kind === 1 ? 'text' : h.kind === 2 ? 'read' : h.kind === 3 ? 'write' : undefined,
|
|
765
|
+
}));
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Get folding ranges (code regions, procedures, etc.)
|
|
769
|
+
*/
|
|
770
|
+
async getFoldingRanges(uri) {
|
|
771
|
+
await this.ensureInitialized();
|
|
772
|
+
await this.openDocument(uri);
|
|
773
|
+
const params = {
|
|
774
|
+
textDocument: { uri },
|
|
775
|
+
};
|
|
776
|
+
const result = await this.connection.sendRequest('textDocument/foldingRange', params);
|
|
777
|
+
if (!result)
|
|
778
|
+
return [];
|
|
779
|
+
return result.map(r => ({
|
|
780
|
+
startLine: r.startLine,
|
|
781
|
+
startCharacter: r.startCharacter,
|
|
782
|
+
endLine: r.endLine,
|
|
783
|
+
endCharacter: r.endCharacter,
|
|
784
|
+
kind: r.kind,
|
|
785
|
+
}));
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Get selection ranges (smart expand/shrink selection)
|
|
789
|
+
*/
|
|
790
|
+
async getSelectionRanges(uri, positions) {
|
|
791
|
+
await this.ensureInitialized();
|
|
792
|
+
await this.openDocument(uri);
|
|
793
|
+
const params = {
|
|
794
|
+
textDocument: { uri },
|
|
795
|
+
positions,
|
|
796
|
+
};
|
|
797
|
+
const result = await this.connection.sendRequest('textDocument/selectionRange', params);
|
|
798
|
+
if (!result)
|
|
799
|
+
return [];
|
|
800
|
+
const convertSelectionRange = (sr) => ({
|
|
801
|
+
range: {
|
|
802
|
+
start: { line: sr.range.start.line, character: sr.range.start.character },
|
|
803
|
+
end: { line: sr.range.end.line, character: sr.range.end.character },
|
|
804
|
+
},
|
|
805
|
+
parent: sr.parent ? convertSelectionRange(sr.parent) : undefined,
|
|
806
|
+
});
|
|
807
|
+
return result.map(convertSelectionRange);
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Go to type definition (variable's type)
|
|
811
|
+
*/
|
|
812
|
+
async getTypeDefinition(uri, line, character) {
|
|
813
|
+
await this.ensureInitialized();
|
|
814
|
+
await this.openDocument(uri);
|
|
815
|
+
const params = {
|
|
816
|
+
textDocument: { uri },
|
|
817
|
+
position: { line, character },
|
|
818
|
+
};
|
|
819
|
+
const result = await this.connection.sendRequest('textDocument/typeDefinition', params);
|
|
820
|
+
if (!result)
|
|
821
|
+
return [];
|
|
822
|
+
return Array.isArray(result) ? result : [result];
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Go to implementation (interface implementations)
|
|
826
|
+
*/
|
|
827
|
+
async getImplementation(uri, line, character) {
|
|
828
|
+
await this.ensureInitialized();
|
|
829
|
+
await this.openDocument(uri);
|
|
830
|
+
const params = {
|
|
831
|
+
textDocument: { uri },
|
|
832
|
+
position: { line, character },
|
|
833
|
+
};
|
|
834
|
+
const result = await this.connection.sendRequest('textDocument/implementation', params);
|
|
835
|
+
if (!result)
|
|
836
|
+
return [];
|
|
837
|
+
return Array.isArray(result) ? result : [result];
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Format on type (format after specific character like ';')
|
|
841
|
+
*/
|
|
842
|
+
async formatOnType(uri, line, character, ch, options) {
|
|
843
|
+
await this.ensureInitialized();
|
|
844
|
+
await this.openDocument(uri);
|
|
845
|
+
const params = {
|
|
846
|
+
textDocument: { uri },
|
|
847
|
+
position: { line, character },
|
|
848
|
+
ch,
|
|
849
|
+
options: {
|
|
850
|
+
tabSize: options?.tabSize ?? 4,
|
|
851
|
+
insertSpaces: options?.insertSpaces ?? true,
|
|
852
|
+
},
|
|
853
|
+
};
|
|
854
|
+
const result = await this.connection.sendRequest('textDocument/onTypeFormatting', params);
|
|
855
|
+
if (!result)
|
|
856
|
+
return [];
|
|
857
|
+
return result.map(edit => ({
|
|
858
|
+
range: {
|
|
859
|
+
start: { line: edit.range.start.line, character: edit.range.start.character },
|
|
860
|
+
end: { line: edit.range.end.line, character: edit.range.end.character },
|
|
861
|
+
},
|
|
862
|
+
newText: edit.newText,
|
|
863
|
+
}));
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Get code lenses (inline hints like reference counts)
|
|
867
|
+
*/
|
|
868
|
+
async getCodeLenses(uri) {
|
|
869
|
+
await this.ensureInitialized();
|
|
870
|
+
await this.openDocument(uri);
|
|
871
|
+
const params = {
|
|
872
|
+
textDocument: { uri },
|
|
873
|
+
};
|
|
874
|
+
const result = await this.connection.sendRequest('textDocument/codeLens', params);
|
|
875
|
+
if (!result)
|
|
876
|
+
return [];
|
|
877
|
+
return result.map(lens => ({
|
|
878
|
+
range: {
|
|
879
|
+
start: { line: lens.range.start.line, character: lens.range.start.character },
|
|
880
|
+
end: { line: lens.range.end.line, character: lens.range.end.character },
|
|
881
|
+
},
|
|
882
|
+
command: lens.command ? {
|
|
883
|
+
title: lens.command.title,
|
|
884
|
+
command: lens.command.command,
|
|
885
|
+
arguments: lens.command.arguments,
|
|
886
|
+
} : undefined,
|
|
887
|
+
data: lens.data,
|
|
888
|
+
}));
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Resolve a code lens (get the command for a lens)
|
|
892
|
+
*/
|
|
893
|
+
async resolveCodeLens(lens) {
|
|
894
|
+
await this.ensureInitialized();
|
|
895
|
+
const lspLens = {
|
|
896
|
+
range: {
|
|
897
|
+
start: { line: lens.range.start.line, character: lens.range.start.character },
|
|
898
|
+
end: { line: lens.range.end.line, character: lens.range.end.character },
|
|
899
|
+
},
|
|
900
|
+
data: lens.data,
|
|
901
|
+
};
|
|
902
|
+
const result = await this.connection.sendRequest('codeLens/resolve', lspLens);
|
|
903
|
+
return {
|
|
904
|
+
range: {
|
|
905
|
+
start: { line: result.range.start.line, character: result.range.start.character },
|
|
906
|
+
end: { line: result.range.end.line, character: result.range.end.character },
|
|
907
|
+
},
|
|
908
|
+
command: result.command ? {
|
|
909
|
+
title: result.command.title,
|
|
910
|
+
command: result.command.command,
|
|
911
|
+
arguments: result.command.arguments,
|
|
912
|
+
} : undefined,
|
|
913
|
+
data: result.data,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Get document links (clickable URLs in comments)
|
|
918
|
+
*/
|
|
919
|
+
async getDocumentLinks(uri) {
|
|
920
|
+
await this.ensureInitialized();
|
|
921
|
+
await this.openDocument(uri);
|
|
922
|
+
const params = {
|
|
923
|
+
textDocument: { uri },
|
|
924
|
+
};
|
|
925
|
+
const result = await this.connection.sendRequest('textDocument/documentLink', params);
|
|
926
|
+
if (!result)
|
|
927
|
+
return [];
|
|
928
|
+
return result.map(link => ({
|
|
929
|
+
range: {
|
|
930
|
+
start: { line: link.range.start.line, character: link.range.start.character },
|
|
931
|
+
end: { line: link.range.end.line, character: link.range.end.character },
|
|
932
|
+
},
|
|
933
|
+
target: link.target,
|
|
934
|
+
tooltip: link.tooltip,
|
|
935
|
+
}));
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Execute a command (e.g., from code action)
|
|
939
|
+
*/
|
|
940
|
+
async executeCommand(command, args) {
|
|
941
|
+
await this.ensureInitialized();
|
|
942
|
+
const params = {
|
|
943
|
+
command,
|
|
944
|
+
arguments: args,
|
|
945
|
+
};
|
|
946
|
+
return this.connection.sendRequest('workspace/executeCommand', params);
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Get semantic tokens (for syntax highlighting)
|
|
950
|
+
*/
|
|
951
|
+
async getSemanticTokens(uri) {
|
|
952
|
+
await this.ensureInitialized();
|
|
953
|
+
await this.openDocument(uri);
|
|
954
|
+
const params = {
|
|
955
|
+
textDocument: { uri },
|
|
956
|
+
};
|
|
957
|
+
const result = await this.connection.sendRequest('textDocument/semanticTokens/full', params);
|
|
958
|
+
if (!result)
|
|
959
|
+
return null;
|
|
960
|
+
return {
|
|
961
|
+
resultId: result.resultId,
|
|
962
|
+
data: result.data,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Get semantic tokens for a range
|
|
967
|
+
*/
|
|
968
|
+
async getSemanticTokensRange(uri, range) {
|
|
969
|
+
await this.ensureInitialized();
|
|
970
|
+
await this.openDocument(uri);
|
|
971
|
+
const params = {
|
|
972
|
+
textDocument: { uri },
|
|
973
|
+
range,
|
|
974
|
+
};
|
|
975
|
+
const result = await this.connection.sendRequest('textDocument/semanticTokens/range', params);
|
|
976
|
+
if (!result)
|
|
977
|
+
return null;
|
|
978
|
+
return {
|
|
979
|
+
resultId: result.resultId,
|
|
980
|
+
data: result.data,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Restart the language server (useful when it hangs or after external changes)
|
|
985
|
+
*/
|
|
986
|
+
async restart() {
|
|
987
|
+
this.logger.info('Restarting AL Language Server...');
|
|
988
|
+
await this.shutdown();
|
|
989
|
+
await this.initialize();
|
|
990
|
+
this.logger.info('AL Language Server restarted successfully');
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Find all symbols that reference a given symbol (enhanced reference navigation)
|
|
994
|
+
* Returns referencing symbols with context around the reference
|
|
995
|
+
*/
|
|
996
|
+
async findReferencingSymbols(uri, line, character, options) {
|
|
997
|
+
await this.ensureInitialized();
|
|
998
|
+
await this.openDocument(uri);
|
|
999
|
+
const contextBefore = options?.contextLinesBefore ?? 1;
|
|
1000
|
+
const contextAfter = options?.contextLinesAfter ?? 1;
|
|
1001
|
+
// Get all references
|
|
1002
|
+
const params = {
|
|
1003
|
+
textDocument: { uri },
|
|
1004
|
+
position: { line, character },
|
|
1005
|
+
context: { includeDeclaration: options?.includeDeclaration ?? false },
|
|
1006
|
+
};
|
|
1007
|
+
const locations = await this.connection.sendRequest('textDocument/references', params);
|
|
1008
|
+
if (!locations || locations.length === 0) {
|
|
1009
|
+
return [];
|
|
1010
|
+
}
|
|
1011
|
+
const results = [];
|
|
1012
|
+
for (const loc of locations) {
|
|
1013
|
+
const result = {
|
|
1014
|
+
location: loc,
|
|
1015
|
+
};
|
|
1016
|
+
// Try to get the containing symbol
|
|
1017
|
+
try {
|
|
1018
|
+
const symbols = await this.getDocumentSymbols(loc.uri);
|
|
1019
|
+
const containingSymbol = this.findSymbolAtPosition(symbols, loc.range.start.line, loc.range.start.character);
|
|
1020
|
+
if (containingSymbol) {
|
|
1021
|
+
result.containingSymbol = containingSymbol;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
// Ignore errors getting symbols
|
|
1026
|
+
}
|
|
1027
|
+
// Try to get context snippet
|
|
1028
|
+
try {
|
|
1029
|
+
const filePath = this.uriToPath(loc.uri);
|
|
1030
|
+
if (fs.existsSync(filePath)) {
|
|
1031
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1032
|
+
const lines = content.split('\n');
|
|
1033
|
+
const startLine = Math.max(0, loc.range.start.line - contextBefore);
|
|
1034
|
+
const endLine = Math.min(lines.length - 1, loc.range.start.line + contextAfter);
|
|
1035
|
+
result.contextSnippet = lines.slice(startLine, endLine + 1).join('\n');
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
catch {
|
|
1039
|
+
// Ignore errors reading file
|
|
1040
|
+
}
|
|
1041
|
+
results.push(result);
|
|
1042
|
+
}
|
|
1043
|
+
return results;
|
|
1044
|
+
}
|
|
406
1045
|
/**
|
|
407
1046
|
* Shutdown the language server
|
|
408
1047
|
*/
|
|
@@ -427,5 +1066,4 @@ class ALLanguageServer {
|
|
|
427
1066
|
this.diagnosticsCache.clear();
|
|
428
1067
|
}
|
|
429
1068
|
}
|
|
430
|
-
exports.ALLanguageServer = ALLanguageServer;
|
|
431
1069
|
//# sourceMappingURL=language-server.js.map
|