partnercore-proxy 0.1.5 → 0.4.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.
@@ -3,7 +3,7 @@
3
3
  * AL Language Server Client
4
4
  *
5
5
  * Communicates with the Microsoft AL Language Server via LSP protocol.
6
- * Based on insights from Serena project's implementation.
6
+ * Provides full LSP integration for AL development.
7
7
  */
8
8
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
9
  if (k2 === undefined) k2 = k;
@@ -146,6 +146,91 @@ class ALLanguageServer {
146
146
  publishDiagnostics: {
147
147
  relatedInformation: true,
148
148
  },
149
+ codeAction: {
150
+ dynamicRegistration: true,
151
+ codeActionLiteralSupport: {
152
+ codeActionKind: {
153
+ valueSet: [
154
+ 'quickfix',
155
+ 'refactor',
156
+ 'refactor.extract',
157
+ 'refactor.inline',
158
+ 'refactor.rewrite',
159
+ 'source',
160
+ 'source.organizeImports',
161
+ ],
162
+ },
163
+ },
164
+ resolveSupport: {
165
+ properties: ['edit'],
166
+ },
167
+ },
168
+ signatureHelp: {
169
+ dynamicRegistration: true,
170
+ signatureInformation: {
171
+ documentationFormat: ['markdown', 'plaintext'],
172
+ parameterInformation: {
173
+ labelOffsetSupport: true,
174
+ },
175
+ },
176
+ contextSupport: true,
177
+ },
178
+ formatting: {
179
+ dynamicRegistration: true,
180
+ },
181
+ rangeFormatting: {
182
+ dynamicRegistration: true,
183
+ },
184
+ onTypeFormatting: {
185
+ dynamicRegistration: true,
186
+ },
187
+ documentHighlight: {
188
+ dynamicRegistration: true,
189
+ },
190
+ foldingRange: {
191
+ dynamicRegistration: true,
192
+ foldingRangeKind: {
193
+ valueSet: ['comment', 'imports', 'region'],
194
+ },
195
+ },
196
+ selectionRange: {
197
+ dynamicRegistration: true,
198
+ },
199
+ codeLens: {
200
+ dynamicRegistration: true,
201
+ },
202
+ documentLink: {
203
+ dynamicRegistration: true,
204
+ tooltipSupport: true,
205
+ },
206
+ typeDefinition: {
207
+ dynamicRegistration: true,
208
+ linkSupport: true,
209
+ },
210
+ implementation: {
211
+ dynamicRegistration: true,
212
+ linkSupport: true,
213
+ },
214
+ semanticTokens: {
215
+ dynamicRegistration: true,
216
+ tokenTypes: [
217
+ 'namespace', 'type', 'class', 'enum', 'interface', 'struct',
218
+ 'typeParameter', 'parameter', 'variable', 'property', 'enumMember',
219
+ 'event', 'function', 'method', 'macro', 'keyword', 'modifier',
220
+ 'comment', 'string', 'number', 'regexp', 'operator', 'decorator',
221
+ ],
222
+ tokenModifiers: [
223
+ 'declaration', 'definition', 'readonly', 'static', 'deprecated',
224
+ 'abstract', 'async', 'modification', 'documentation', 'defaultLibrary',
225
+ ],
226
+ formats: ['relative'],
227
+ requests: {
228
+ full: true,
229
+ range: true,
230
+ },
231
+ multilineTokenSupport: false,
232
+ overlappingTokenSupport: false,
233
+ },
149
234
  },
150
235
  workspace: {
151
236
  applyEdit: true,
@@ -313,6 +398,269 @@ class ALLanguageServer {
313
398
  const result = await this.connection.sendRequest('workspace/symbol', { query });
314
399
  return result || [];
315
400
  }
401
+ /**
402
+ * Update document content (for editing)
403
+ */
404
+ async updateDocument(uri, content, version) {
405
+ if (!this.connection) {
406
+ throw new Error('Language server not initialized');
407
+ }
408
+ // If document is not open, open it first
409
+ if (!this.openDocuments.has(uri)) {
410
+ await this.openDocument(uri);
411
+ }
412
+ const params = {
413
+ textDocument: { uri, version },
414
+ contentChanges: [{ text: content }],
415
+ };
416
+ void this.connection.sendNotification('textDocument/didChange', params);
417
+ // Wait for diagnostics to be processed
418
+ await new Promise(resolve => setTimeout(resolve, 100));
419
+ }
420
+ /**
421
+ * Rename symbol across the workspace
422
+ * @returns WorkspaceEdit with all changes needed
423
+ */
424
+ async renameSymbol(uri, line, character, newName) {
425
+ await this.ensureInitialized();
426
+ await this.openDocument(uri);
427
+ // First, check if rename is valid
428
+ const prepareParams = {
429
+ textDocument: { uri },
430
+ position: { line, character },
431
+ };
432
+ try {
433
+ const prepareResult = await this.connection.sendRequest('textDocument/prepareRename', prepareParams);
434
+ if (!prepareResult) {
435
+ this.logger.debug('Rename not available at this position');
436
+ return null;
437
+ }
438
+ // Perform the rename
439
+ const renameParams = {
440
+ textDocument: { uri },
441
+ position: { line, character },
442
+ newName,
443
+ };
444
+ const result = await this.connection.sendRequest('textDocument/rename', renameParams);
445
+ return result;
446
+ }
447
+ catch (error) {
448
+ this.logger.error('Rename failed:', error);
449
+ return null;
450
+ }
451
+ }
452
+ /**
453
+ * Get the symbol at a specific position
454
+ */
455
+ async getSymbolAtPosition(uri, line, character) {
456
+ const symbols = await this.getDocumentSymbols(uri);
457
+ return this.findSymbolAtPosition(symbols, line, character);
458
+ }
459
+ /**
460
+ * Get code actions (quick fixes, refactorings) at a specific position or range
461
+ */
462
+ async getCodeActions(uri, range, context) {
463
+ await this.ensureInitialized();
464
+ await this.openDocument(uri);
465
+ // Build diagnostics with proper typing
466
+ const diagnostics = context?.diagnostics?.map(d => ({
467
+ message: d.message,
468
+ severity: this.severityToNumber(d.severity),
469
+ range: d.range,
470
+ code: d.code,
471
+ source: d.source,
472
+ })) || [];
473
+ const params = {
474
+ textDocument: { uri },
475
+ range,
476
+ context: {
477
+ diagnostics,
478
+ only: context?.only,
479
+ },
480
+ };
481
+ const result = await this.connection.sendRequest('textDocument/codeAction', params);
482
+ if (!result)
483
+ return [];
484
+ return result.map(item => {
485
+ // Check if it's a CodeAction (has 'title' as required field for both, but CodeAction has more fields)
486
+ const isCodeAction = 'kind' in item || 'edit' in item || ('command' in item && typeof item.command === 'object');
487
+ if (isCodeAction) {
488
+ const action = item;
489
+ let editResult;
490
+ if (action.edit && action.edit.changes) {
491
+ editResult = {
492
+ changes: action.edit.changes,
493
+ };
494
+ }
495
+ return {
496
+ title: action.title,
497
+ kind: action.kind,
498
+ isPreferred: action.isPreferred,
499
+ edit: editResult,
500
+ command: action.command ? {
501
+ title: action.command.title,
502
+ command: action.command.command,
503
+ arguments: action.command.arguments,
504
+ } : undefined,
505
+ };
506
+ }
507
+ else {
508
+ // It's a Command (simpler structure: title, command string, arguments)
509
+ const cmd = item;
510
+ return {
511
+ title: cmd.title,
512
+ command: {
513
+ title: cmd.title,
514
+ command: cmd.command,
515
+ arguments: cmd.arguments,
516
+ },
517
+ };
518
+ }
519
+ });
520
+ }
521
+ /**
522
+ * Get signature help (function parameter hints) at a position
523
+ */
524
+ async getSignatureHelp(uri, line, character, context) {
525
+ await this.ensureInitialized();
526
+ await this.openDocument(uri);
527
+ const params = {
528
+ textDocument: { uri },
529
+ position: { line, character },
530
+ context: context ? {
531
+ triggerKind: context.triggerKind || 1,
532
+ triggerCharacter: context.triggerCharacter,
533
+ isRetrigger: context.isRetrigger || false,
534
+ } : undefined,
535
+ };
536
+ const result = await this.connection.sendRequest('textDocument/signatureHelp', params);
537
+ if (!result)
538
+ return null;
539
+ return {
540
+ signatures: result.signatures.map(sig => ({
541
+ label: sig.label,
542
+ documentation: typeof sig.documentation === 'string'
543
+ ? sig.documentation
544
+ : sig.documentation?.value,
545
+ parameters: sig.parameters?.map(p => ({
546
+ label: p.label,
547
+ documentation: typeof p.documentation === 'string'
548
+ ? p.documentation
549
+ : p.documentation?.value,
550
+ })),
551
+ })),
552
+ activeSignature: result.activeSignature,
553
+ activeParameter: result.activeParameter,
554
+ };
555
+ }
556
+ /**
557
+ * Format an entire document
558
+ */
559
+ async formatDocument(uri, options) {
560
+ await this.ensureInitialized();
561
+ await this.openDocument(uri);
562
+ const formattingOptions = {
563
+ tabSize: options?.tabSize ?? 4,
564
+ insertSpaces: options?.insertSpaces ?? true,
565
+ };
566
+ const params = {
567
+ textDocument: { uri },
568
+ options: formattingOptions,
569
+ };
570
+ const result = await this.connection.sendRequest('textDocument/formatting', params);
571
+ if (!result)
572
+ return [];
573
+ return result.map(edit => ({
574
+ range: {
575
+ start: { line: edit.range.start.line, character: edit.range.start.character },
576
+ end: { line: edit.range.end.line, character: edit.range.end.character },
577
+ },
578
+ newText: edit.newText,
579
+ }));
580
+ }
581
+ /**
582
+ * Format a range within a document
583
+ */
584
+ async formatRange(uri, range, options) {
585
+ await this.ensureInitialized();
586
+ await this.openDocument(uri);
587
+ const formattingOptions = {
588
+ tabSize: options?.tabSize ?? 4,
589
+ insertSpaces: options?.insertSpaces ?? true,
590
+ };
591
+ const params = {
592
+ textDocument: { uri },
593
+ range,
594
+ options: formattingOptions,
595
+ };
596
+ const result = await this.connection.sendRequest('textDocument/rangeFormatting', params);
597
+ if (!result)
598
+ return [];
599
+ return result.map(edit => ({
600
+ range: {
601
+ start: { line: edit.range.start.line, character: edit.range.start.character },
602
+ end: { line: edit.range.end.line, character: edit.range.end.character },
603
+ },
604
+ newText: edit.newText,
605
+ }));
606
+ }
607
+ /**
608
+ * Convert severity string to LSP severity number
609
+ */
610
+ severityToNumber(severity) {
611
+ switch (severity) {
612
+ case 'error': return 1;
613
+ case 'warning': return 2;
614
+ case 'info': return 3;
615
+ case 'hint': return 4;
616
+ default: return 3;
617
+ }
618
+ }
619
+ /**
620
+ * Find symbol containing the given position
621
+ */
622
+ findSymbolAtPosition(symbols, line, character) {
623
+ for (const symbol of symbols) {
624
+ const { start, end } = symbol.range;
625
+ // Check if position is within this symbol's range
626
+ const isAfterStart = line > start.line || (line === start.line && character >= start.character);
627
+ const isBeforeEnd = line < end.line || (line === end.line && character <= end.character);
628
+ if (isAfterStart && isBeforeEnd) {
629
+ // Check children first (more specific match)
630
+ if (symbol.children) {
631
+ const childMatch = this.findSymbolAtPosition(symbol.children, line, character);
632
+ if (childMatch) {
633
+ return childMatch;
634
+ }
635
+ }
636
+ return symbol;
637
+ }
638
+ }
639
+ return null;
640
+ }
641
+ /**
642
+ * Find a symbol by name in the document
643
+ */
644
+ async findSymbolByName(uri, symbolName) {
645
+ const symbols = await this.getDocumentSymbols(uri);
646
+ return this.searchSymbolByName(symbols, symbolName);
647
+ }
648
+ /**
649
+ * Search for symbol by name recursively
650
+ */
651
+ searchSymbolByName(symbols, name) {
652
+ for (const symbol of symbols) {
653
+ if (symbol.name.toLowerCase() === name.toLowerCase()) {
654
+ return symbol;
655
+ }
656
+ if (symbol.children) {
657
+ const found = this.searchSymbolByName(symbol.children, name);
658
+ if (found)
659
+ return found;
660
+ }
661
+ }
662
+ return null;
663
+ }
316
664
  /**
317
665
  * Convert LSP symbols to our format
318
666
  */
@@ -403,6 +751,333 @@ class ALLanguageServer {
403
751
  await this.initialize();
404
752
  }
405
753
  }
754
+ // ==================== Remaining LSP Features ====================
755
+ /**
756
+ * Close a document (cleanup)
757
+ */
758
+ async closeDocument(uri) {
759
+ await this.ensureInitialized();
760
+ if (!this.openDocuments.has(uri)) {
761
+ return; // Not open
762
+ }
763
+ const params = {
764
+ textDocument: { uri },
765
+ };
766
+ await this.connection.sendNotification('textDocument/didClose', params);
767
+ this.openDocuments.delete(uri);
768
+ this.diagnosticsCache.delete(uri);
769
+ }
770
+ /**
771
+ * Notify save (triggers recompile in some LSPs)
772
+ */
773
+ async saveDocument(uri, text) {
774
+ await this.ensureInitialized();
775
+ await this.openDocument(uri);
776
+ const params = {
777
+ textDocument: { uri },
778
+ text,
779
+ };
780
+ await this.connection.sendNotification('textDocument/didSave', params);
781
+ }
782
+ /**
783
+ * Get document highlights (all occurrences of symbol under cursor)
784
+ */
785
+ async getDocumentHighlights(uri, line, character) {
786
+ await this.ensureInitialized();
787
+ await this.openDocument(uri);
788
+ const params = {
789
+ textDocument: { uri },
790
+ position: { line, character },
791
+ };
792
+ const result = await this.connection.sendRequest('textDocument/documentHighlight', params);
793
+ if (!result)
794
+ return [];
795
+ return result.map(h => ({
796
+ range: {
797
+ start: { line: h.range.start.line, character: h.range.start.character },
798
+ end: { line: h.range.end.line, character: h.range.end.character },
799
+ },
800
+ kind: h.kind === 1 ? 'text' : h.kind === 2 ? 'read' : h.kind === 3 ? 'write' : undefined,
801
+ }));
802
+ }
803
+ /**
804
+ * Get folding ranges (code regions, procedures, etc.)
805
+ */
806
+ async getFoldingRanges(uri) {
807
+ await this.ensureInitialized();
808
+ await this.openDocument(uri);
809
+ const params = {
810
+ textDocument: { uri },
811
+ };
812
+ const result = await this.connection.sendRequest('textDocument/foldingRange', params);
813
+ if (!result)
814
+ return [];
815
+ return result.map(r => ({
816
+ startLine: r.startLine,
817
+ startCharacter: r.startCharacter,
818
+ endLine: r.endLine,
819
+ endCharacter: r.endCharacter,
820
+ kind: r.kind,
821
+ }));
822
+ }
823
+ /**
824
+ * Get selection ranges (smart expand/shrink selection)
825
+ */
826
+ async getSelectionRanges(uri, positions) {
827
+ await this.ensureInitialized();
828
+ await this.openDocument(uri);
829
+ const params = {
830
+ textDocument: { uri },
831
+ positions,
832
+ };
833
+ const result = await this.connection.sendRequest('textDocument/selectionRange', params);
834
+ if (!result)
835
+ return [];
836
+ const convertSelectionRange = (sr) => ({
837
+ range: {
838
+ start: { line: sr.range.start.line, character: sr.range.start.character },
839
+ end: { line: sr.range.end.line, character: sr.range.end.character },
840
+ },
841
+ parent: sr.parent ? convertSelectionRange(sr.parent) : undefined,
842
+ });
843
+ return result.map(convertSelectionRange);
844
+ }
845
+ /**
846
+ * Go to type definition (variable's type)
847
+ */
848
+ async getTypeDefinition(uri, line, character) {
849
+ await this.ensureInitialized();
850
+ await this.openDocument(uri);
851
+ const params = {
852
+ textDocument: { uri },
853
+ position: { line, character },
854
+ };
855
+ const result = await this.connection.sendRequest('textDocument/typeDefinition', params);
856
+ if (!result)
857
+ return [];
858
+ return Array.isArray(result) ? result : [result];
859
+ }
860
+ /**
861
+ * Go to implementation (interface implementations)
862
+ */
863
+ async getImplementation(uri, line, character) {
864
+ await this.ensureInitialized();
865
+ await this.openDocument(uri);
866
+ const params = {
867
+ textDocument: { uri },
868
+ position: { line, character },
869
+ };
870
+ const result = await this.connection.sendRequest('textDocument/implementation', params);
871
+ if (!result)
872
+ return [];
873
+ return Array.isArray(result) ? result : [result];
874
+ }
875
+ /**
876
+ * Format on type (format after specific character like ';')
877
+ */
878
+ async formatOnType(uri, line, character, ch, options) {
879
+ await this.ensureInitialized();
880
+ await this.openDocument(uri);
881
+ const params = {
882
+ textDocument: { uri },
883
+ position: { line, character },
884
+ ch,
885
+ options: {
886
+ tabSize: options?.tabSize ?? 4,
887
+ insertSpaces: options?.insertSpaces ?? true,
888
+ },
889
+ };
890
+ const result = await this.connection.sendRequest('textDocument/onTypeFormatting', params);
891
+ if (!result)
892
+ return [];
893
+ return result.map(edit => ({
894
+ range: {
895
+ start: { line: edit.range.start.line, character: edit.range.start.character },
896
+ end: { line: edit.range.end.line, character: edit.range.end.character },
897
+ },
898
+ newText: edit.newText,
899
+ }));
900
+ }
901
+ /**
902
+ * Get code lenses (inline hints like reference counts)
903
+ */
904
+ async getCodeLenses(uri) {
905
+ await this.ensureInitialized();
906
+ await this.openDocument(uri);
907
+ const params = {
908
+ textDocument: { uri },
909
+ };
910
+ const result = await this.connection.sendRequest('textDocument/codeLens', params);
911
+ if (!result)
912
+ return [];
913
+ return result.map(lens => ({
914
+ range: {
915
+ start: { line: lens.range.start.line, character: lens.range.start.character },
916
+ end: { line: lens.range.end.line, character: lens.range.end.character },
917
+ },
918
+ command: lens.command ? {
919
+ title: lens.command.title,
920
+ command: lens.command.command,
921
+ arguments: lens.command.arguments,
922
+ } : undefined,
923
+ data: lens.data,
924
+ }));
925
+ }
926
+ /**
927
+ * Resolve a code lens (get the command for a lens)
928
+ */
929
+ async resolveCodeLens(lens) {
930
+ await this.ensureInitialized();
931
+ const lspLens = {
932
+ range: {
933
+ start: { line: lens.range.start.line, character: lens.range.start.character },
934
+ end: { line: lens.range.end.line, character: lens.range.end.character },
935
+ },
936
+ data: lens.data,
937
+ };
938
+ const result = await this.connection.sendRequest('codeLens/resolve', lspLens);
939
+ return {
940
+ range: {
941
+ start: { line: result.range.start.line, character: result.range.start.character },
942
+ end: { line: result.range.end.line, character: result.range.end.character },
943
+ },
944
+ command: result.command ? {
945
+ title: result.command.title,
946
+ command: result.command.command,
947
+ arguments: result.command.arguments,
948
+ } : undefined,
949
+ data: result.data,
950
+ };
951
+ }
952
+ /**
953
+ * Get document links (clickable URLs in comments)
954
+ */
955
+ async getDocumentLinks(uri) {
956
+ await this.ensureInitialized();
957
+ await this.openDocument(uri);
958
+ const params = {
959
+ textDocument: { uri },
960
+ };
961
+ const result = await this.connection.sendRequest('textDocument/documentLink', params);
962
+ if (!result)
963
+ return [];
964
+ return result.map(link => ({
965
+ range: {
966
+ start: { line: link.range.start.line, character: link.range.start.character },
967
+ end: { line: link.range.end.line, character: link.range.end.character },
968
+ },
969
+ target: link.target,
970
+ tooltip: link.tooltip,
971
+ }));
972
+ }
973
+ /**
974
+ * Execute a command (e.g., from code action)
975
+ */
976
+ async executeCommand(command, args) {
977
+ await this.ensureInitialized();
978
+ const params = {
979
+ command,
980
+ arguments: args,
981
+ };
982
+ return this.connection.sendRequest('workspace/executeCommand', params);
983
+ }
984
+ /**
985
+ * Get semantic tokens (for syntax highlighting)
986
+ */
987
+ async getSemanticTokens(uri) {
988
+ await this.ensureInitialized();
989
+ await this.openDocument(uri);
990
+ const params = {
991
+ textDocument: { uri },
992
+ };
993
+ const result = await this.connection.sendRequest('textDocument/semanticTokens/full', params);
994
+ if (!result)
995
+ return null;
996
+ return {
997
+ resultId: result.resultId,
998
+ data: result.data,
999
+ };
1000
+ }
1001
+ /**
1002
+ * Get semantic tokens for a range
1003
+ */
1004
+ async getSemanticTokensRange(uri, range) {
1005
+ await this.ensureInitialized();
1006
+ await this.openDocument(uri);
1007
+ const params = {
1008
+ textDocument: { uri },
1009
+ range,
1010
+ };
1011
+ const result = await this.connection.sendRequest('textDocument/semanticTokens/range', params);
1012
+ if (!result)
1013
+ return null;
1014
+ return {
1015
+ resultId: result.resultId,
1016
+ data: result.data,
1017
+ };
1018
+ }
1019
+ /**
1020
+ * Restart the language server (useful when it hangs or after external changes)
1021
+ */
1022
+ async restart() {
1023
+ this.logger.info('Restarting AL Language Server...');
1024
+ await this.shutdown();
1025
+ await this.initialize();
1026
+ this.logger.info('AL Language Server restarted successfully');
1027
+ }
1028
+ /**
1029
+ * Find all symbols that reference a given symbol (enhanced reference navigation)
1030
+ * Returns referencing symbols with context around the reference
1031
+ */
1032
+ async findReferencingSymbols(uri, line, character, options) {
1033
+ await this.ensureInitialized();
1034
+ await this.openDocument(uri);
1035
+ const contextBefore = options?.contextLinesBefore ?? 1;
1036
+ const contextAfter = options?.contextLinesAfter ?? 1;
1037
+ // Get all references
1038
+ const params = {
1039
+ textDocument: { uri },
1040
+ position: { line, character },
1041
+ context: { includeDeclaration: options?.includeDeclaration ?? false },
1042
+ };
1043
+ const locations = await this.connection.sendRequest('textDocument/references', params);
1044
+ if (!locations || locations.length === 0) {
1045
+ return [];
1046
+ }
1047
+ const results = [];
1048
+ for (const loc of locations) {
1049
+ const result = {
1050
+ location: loc,
1051
+ };
1052
+ // Try to get the containing symbol
1053
+ try {
1054
+ const symbols = await this.getDocumentSymbols(loc.uri);
1055
+ const containingSymbol = this.findSymbolAtPosition(symbols, loc.range.start.line, loc.range.start.character);
1056
+ if (containingSymbol) {
1057
+ result.containingSymbol = containingSymbol;
1058
+ }
1059
+ }
1060
+ catch {
1061
+ // Ignore errors getting symbols
1062
+ }
1063
+ // Try to get context snippet
1064
+ try {
1065
+ const filePath = this.uriToPath(loc.uri);
1066
+ if (fs.existsSync(filePath)) {
1067
+ const content = fs.readFileSync(filePath, 'utf-8');
1068
+ const lines = content.split('\n');
1069
+ const startLine = Math.max(0, loc.range.start.line - contextBefore);
1070
+ const endLine = Math.min(lines.length - 1, loc.range.start.line + contextAfter);
1071
+ result.contextSnippet = lines.slice(startLine, endLine + 1).join('\n');
1072
+ }
1073
+ }
1074
+ catch {
1075
+ // Ignore errors reading file
1076
+ }
1077
+ results.push(result);
1078
+ }
1079
+ return results;
1080
+ }
406
1081
  /**
407
1082
  * Shutdown the language server
408
1083
  */