abapgit-agent 1.14.5 → 1.15.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.
@@ -89,6 +89,10 @@ const INCLUDE_TYPE_TO_ADT = {
89
89
  * e.g. ZCL_ABGAGT_AGENT=============CM00D
90
90
  * These must be routed to the programs/includes ADT endpoint.
91
91
  *
92
+ * FUGR source includes follow L<group><suffix> naming (e.g. LSUSRU04, LSUSRTOP,
93
+ * LSUSRF10, L_ABAU01). They start with 'L' and are routed to programs/includes.
94
+ * Customer-namespace programs always start with Z/Y, so this branch is safe.
95
+ *
92
96
  * When includeType is supplied (testclasses|locals_imp|locals_def),
93
97
  * the URI targets the sub-include of the class instead of /source/main.
94
98
  * Line numbers are then section-local (from the .clas.<file>.abap file).
@@ -103,6 +107,26 @@ function objectUri(name, includeType) {
103
107
  }
104
108
  return `/sap/bc/adt/oo/classes/${lower}/source/main`;
105
109
  }
110
+ // FUGR source includes: L<group>U<NN>, L<group>TOP, L<group>F<NN>, etc.
111
+ // All start with 'L'. Customer Z/Y programs never start with 'L'.
112
+ // Correct ADT URI (verified against abap-adt-api):
113
+ // /sap/bc/adt/functions/groups/<group>/includes/<include>/source/main
114
+ // NOT /programs/includes/ — that path is for standalone PROG/I includes only.
115
+ // Group name is derived by stripping leading 'L' and trailing suffix:
116
+ // U<NN> — FM source include (U01, U02, ...)
117
+ // TOP — pool include
118
+ // F<NN> — form include
119
+ // XX — internal include
120
+ if (/^L/.test(upper)) {
121
+ const withoutL = upper.slice(1); // e.g. ZCAIS_DEMOU01
122
+ const group = withoutL
123
+ .replace(/U\d+$/, '') // strip Unn suffix
124
+ .replace(/TOP$/, '') // strip TOP suffix
125
+ .replace(/F\d+$/, '') // strip Fnn suffix
126
+ .replace(/XX$/, '') // strip XX suffix
127
+ .toLowerCase();
128
+ return `/sap/bc/adt/functions/groups/${group}/includes/${lower}/source/main`;
129
+ }
106
130
  return `/sap/bc/adt/programs/programs/${lower}`;
107
131
  }
108
132
 
@@ -227,15 +251,25 @@ async function refreshBreakpoints(config, adt, bps) {
227
251
 
228
252
  const serverResults = parseBreakpointResponse(resp.body || '', bps);
229
253
 
230
- // Match server results back to local bps by uri+line
254
+ // Match server results back to local bps by uri+line.
255
+ // ADT may return a different canonical URI than what was sent (e.g. it rewrites
256
+ // /functions/groups/<g>/includes/<inc>/... to /functions/groups/<g>/fmodules/<fm>/...).
257
+ // Fall back to matching by line alone when URI doesn't match, then adopt the
258
+ // server's canonical URI so future refreshes continue to work.
231
259
  const valid = [];
232
260
  const stale = [];
233
261
  for (const bp of bps) {
234
- const match = serverResults.find(r => r.uri === bp.uri && r.line === bp.line);
262
+ let match = serverResults.find(r => r.uri === bp.uri && r.line === bp.line);
263
+ if (!match) {
264
+ // Fallback: match by line number alone (handles URI canonicalization by ADT)
265
+ match = serverResults.find(r => r.line === bp.line);
266
+ }
235
267
  if (match && match.error) {
236
268
  stale.push({ ...bp, error: match.error });
237
269
  } else if (match && match.id) {
238
- valid.push({ ...bp, id: match.id });
270
+ // Adopt the server's canonical URI so subsequent refreshes match correctly
271
+ const canonicalUri = match.uri || bp.uri;
272
+ valid.push({ ...bp, id: match.id, uri: canonicalUri });
239
273
  } else {
240
274
  // No match in response — server silently dropped it (e.g. expired)
241
275
  stale.push({ ...bp, error: 'Not registered on server' });
@@ -389,9 +423,16 @@ async function cmdSet(args, config, adt) {
389
423
  }
390
424
 
391
425
  // Update local state with server-assigned IDs
426
+ // Use URI+line match first; fall back to line-only for FUGR where ADT rewrites
427
+ // /includes/<inc>/ to /fmodules/<fm>/ in the response.
392
428
  const updatedWithServerIds = updated.map(bp => {
393
- const sr = serverResults.find(r => r.uri === bp.uri && r.line === bp.line);
394
- return sr && sr.id ? { ...bp, id: sr.id } : bp;
429
+ const sr = serverResults.find(r => r.uri === bp.uri && r.line === bp.line)
430
+ || serverResults.find(r => r.line === bp.line);
431
+ if (sr && sr.id) {
432
+ const canonicalUri = sr.uri || bp.uri;
433
+ return { ...bp, id: sr.id, uri: canonicalUri };
434
+ }
435
+ return bp;
395
436
  });
396
437
  if (_saveBpState) _saveBpState(config, updatedWithServerIds);
397
438
 
@@ -426,7 +467,8 @@ async function cmdSet(args, config, adt) {
426
467
 
427
468
  if (jsonOutput) {
428
469
  const out = added.map(a => {
429
- const sr = serverResults.find(r => r.uri === a.uri && r.line === a.line);
470
+ const sr = serverResults.find(r => r.uri === a.uri && r.line === a.line)
471
+ || serverResults.find(r => r.line === a.line);
430
472
  return { id: (sr && sr.id) || null, object: a.name, line: a.line };
431
473
  });
432
474
  console.log(JSON.stringify(out.length === 1 ? out[0] : out));
@@ -154,7 +154,9 @@ function findFirstExecutableLine(lines) {
154
154
  const declPattern = /^\s*(data|final|types|constants|class-data)[\s:(]/i;
155
155
  const methodPattern = /^\s*method\s+/i;
156
156
  const commentPattern = /^\s*[*"]/;
157
- let inDeclBlock = false; // true while inside a multi-line DATA:/TYPES: block
157
+ // Program-level header/declaration keywords that are not executable statements
158
+ const progDeclPattern = /^\s*(report|program|parameters|tables|selection-screen|select-options|class-pool|function-pool|interface-pool|type-pool|include)\b/i;
159
+ let inDeclBlock = false; // true while inside a multi-line DATA:/TYPES:/PARAMETERS: block
158
160
  for (let i = 0; i < lines.length; i++) {
159
161
  const trimmed = lines[i].trim();
160
162
  if (inDeclBlock) {
@@ -170,6 +172,11 @@ function findFirstExecutableLine(lines) {
170
172
  if (!trimmed.endsWith('.')) inDeclBlock = true;
171
173
  continue;
172
174
  }
175
+ if (progDeclPattern.test(trimmed)) {
176
+ // Multi-line block (PARAMETERS: ...,\n ...) stays open until period
177
+ if (!trimmed.endsWith('.')) inDeclBlock = true;
178
+ continue;
179
+ }
173
180
  return i;
174
181
  }
175
182
  return 0;
@@ -201,6 +208,15 @@ module.exports = {
201
208
  const jsonOutput = args.includes('--json');
202
209
  const fullMode = args.includes('--full');
203
210
  const linesMode = args.includes('--lines');
211
+ const fmArgIndex = args.indexOf('--fm');
212
+ const fmName = fmArgIndex !== -1 && fmArgIndex + 1 < args.length
213
+ ? args[fmArgIndex + 1].toUpperCase()
214
+ : null;
215
+
216
+ if (fmName && !fullMode) {
217
+ console.error(' Error: --fm requires --full');
218
+ process.exit(1);
219
+ }
204
220
 
205
221
  console.log(`\n Viewing ${objects.length} object(s)`);
206
222
 
@@ -220,6 +236,10 @@ module.exports = {
220
236
  data.full = true;
221
237
  }
222
238
 
239
+ if (fmName) {
240
+ data.fm = fmName;
241
+ }
242
+
223
243
  const result = await http.post('/sap/bc/z_abapgit_agent/view', data, { csrfToken });
224
244
 
225
245
  // Handle uppercase keys from ABAP
@@ -290,6 +310,7 @@ module.exports = {
290
310
  const file = section.FILE || section.file || '';
291
311
  const lines = section.LINES || section.lines || [];
292
312
  const isCmSection = suffix.startsWith('CM') && methodName;
313
+ const isFugrFmSection = !isCmSection && !!methodName;
293
314
 
294
315
  if (linesMode) {
295
316
  // --full --lines: dual line numbers (G [N]) for debugging
@@ -315,10 +336,44 @@ module.exports = {
315
336
  bpHint = `debug set --objects ${objName}:<global_line>`;
316
337
  }
317
338
  console.log(` * ---- Method: ${methodName} (${suffix}) — breakpoint: ${bpHint} ----`);
339
+ } else if (isFugrFmSection) {
340
+ // Find first executable line: skip FUNCTION header, comments, blanks,
341
+ // and declaration blocks (DATA:, CONSTANTS:, TYPES:, etc.)
342
+ const declPat = /^\s*(data|final|types|constants|class-data)[\s:(]/i;
343
+ let firstExecLine = 1;
344
+ let inDecl = false;
345
+ for (let li = 0; li < lines.length; li++) {
346
+ const t = lines[li].trim();
347
+ if (inDecl) {
348
+ if (t.endsWith('.')) inDecl = false;
349
+ continue;
350
+ }
351
+ if (!t) continue;
352
+ if (/^\*/.test(t)) continue;
353
+ if (/^"/.test(t)) continue;
354
+ if (/^function\s+/i.test(t)) continue;
355
+ if (declPat.test(t)) {
356
+ if (!t.endsWith('.')) inDecl = true;
357
+ continue;
358
+ }
359
+ firstExecLine = li + 1;
360
+ break;
361
+ }
362
+ const bpHint = `debug set --objects ${suffix}:${firstExecLine}`;
363
+ console.log(` * ---- FM: ${methodName} (${suffix}) — breakpoint: ${bpHint} ----`);
318
364
  } else if (file) {
319
365
  console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (from .clas.${file}.abap) ----`);
320
366
  } else if (suffix) {
321
- console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (${suffix}) ----`);
367
+ // For program source sections, emit a breakpoint hint at the first executable line.
368
+ const isProgSection = suffix === 'PROG' || suffix === 'prog';
369
+ if (isProgSection) {
370
+ const execOffset = findFirstExecutableLine(lines);
371
+ const execLine = execOffset + 1; // 1-based
372
+ const bpHint = `debug set --objects ${objName}:${execLine}`;
373
+ console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (${suffix}) — breakpoint: ${bpHint} ----`);
374
+ } else {
375
+ console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (${suffix}) ----`);
376
+ }
322
377
  }
323
378
 
324
379
  let includeRelLine = 0;
@@ -334,6 +389,10 @@ module.exports = {
334
389
  const gStr = globalLine ? String(globalLine).padStart(4) : ' ';
335
390
  const iStr = String(includeRelLine).padStart(3);
336
391
  console.log(` ${gStr} [${iStr}] ${codeLine}`);
392
+ } else if (isFugrFmSection) {
393
+ // FM include: line numbers are include-relative = ADT line numbers
394
+ const lStr = String(includeRelLine).padStart(4);
395
+ console.log(` ${lStr} ${codeLine}`);
337
396
  } else {
338
397
  // For sub-include sections with a known ADT include type,
339
398
  // detect METHOD..ENDMETHOD blocks and emit breakpoint hints.
@@ -363,6 +422,8 @@ module.exports = {
363
422
  // --full (no --lines): clean source, no line numbers
364
423
  if (isCmSection) {
365
424
  console.log(` * ---- Method: ${methodName} (${suffix}) ----`);
425
+ } else if (isFugrFmSection) {
426
+ console.log(` * ---- FM: ${methodName} (${suffix}) ----`);
366
427
  } else if (file) {
367
428
  console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (from .clas.${file}.abap) ----`);
368
429
  } else if (suffix) {
@@ -515,6 +515,8 @@ async function getTopic(topic) {
515
515
  // Map topic to local guideline file
516
516
  const guidelineMap = {
517
517
  'abapgit': 'abapgit.md',
518
+ 'abapgit-xml-only': 'abapgit-xml-only.md',
519
+ 'abapgit-fugr': 'abapgit-fugr.md',
518
520
  'xml': 'objects.md',
519
521
  'objects': 'objects.md',
520
522
  'naming': 'objects.md'