@nocturnium/svelte-ide 1.1.1 → 1.2.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.
Files changed (33) hide show
  1. package/dist/components/editor/CognitiveLoadMeter.svelte +27 -0
  2. package/dist/components/editor/ComplexityHeatLayer.svelte +157 -0
  3. package/dist/components/editor/ComplexityHeatLayer.svelte.d.ts +24 -0
  4. package/dist/components/editor/ComplexityLayer.svelte +325 -109
  5. package/dist/components/editor/ComplexityLayer.svelte.d.ts +13 -0
  6. package/dist/components/editor/ConflictZoneLayer.svelte +22 -15
  7. package/dist/components/editor/CustomEditor.svelte +80 -1
  8. package/dist/components/editor/CustomEditor.svelte.d.ts +3 -1
  9. package/dist/components/editor/EchoCursorLayer.svelte +60 -0
  10. package/dist/components/editor/PluginPreviewSandbox.svelte +43 -9
  11. package/dist/components/editor/PluginPreviewSandbox.svelte.d.ts +4 -4
  12. package/dist/components/editor/core/complexity-analyzer.d.ts +31 -0
  13. package/dist/components/editor/core/complexity-analyzer.js +479 -29
  14. package/dist/components/editor/core/conflict-predictor.d.ts +32 -0
  15. package/dist/components/editor/core/conflict-predictor.js +55 -0
  16. package/dist/components/editor/core/crdt-binding.d.ts +4 -0
  17. package/dist/components/editor/core/crdt-binding.js +34 -9
  18. package/dist/components/editor/core/echo-cursor.d.ts +18 -1
  19. package/dist/components/editor/core/echo-cursor.js +117 -6
  20. package/dist/components/editor/core/extract-function.d.ts +27 -0
  21. package/dist/components/editor/core/extract-function.js +865 -0
  22. package/dist/components/editor/core/index.d.ts +1 -0
  23. package/dist/components/editor/core/index.js +1 -0
  24. package/dist/components/editor/core/state.d.ts +38 -5
  25. package/dist/components/editor/core/state.js +175 -98
  26. package/dist/components/editor/core/timeline.js +6 -1
  27. package/dist/components/editor/editor-find.js +15 -3
  28. package/dist/components/editor/theme.d.ts +8 -0
  29. package/dist/components/editor/theme.js +52 -0
  30. package/dist/services/lsp-client.d.ts +3 -0
  31. package/dist/services/lsp-client.js +86 -14
  32. package/dist/styles/theme.css +4 -1
  33. package/package.json +1 -1
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * Based on cognitive complexity research and common code quality metrics.
8
8
  */
9
+ import { resolveLanguage, tokenize } from '../tokenizer';
9
10
  /**
10
11
  * Thresholds for complexity levels
11
12
  */
@@ -15,20 +16,7 @@ const THRESHOLDS = {
15
16
  high: 70,
16
17
  critical: 85
17
18
  };
18
- /**
19
- * Weights for different complexity factors
20
- */
21
- const WEIGHTS = {
22
- // Nesting is the dominant cognitive-load factor, but 15/level over-rated
23
- // shallow code: a function with two nested `if`s landed at ~71 ("High").
24
- // 12/level keeps deeply-nested code critical while letting genuinely medium
25
- // code read as medium.
26
- nestingDepth: 12,
27
- branchingFactor: 8,
28
- lineCount: 0.3,
29
- identifierCount: 0.2,
30
- callCount: 0.5
31
- };
19
+ const COGNITIVE_SCORE_MULTIPLIER = 7;
32
20
  /**
33
21
  * Patterns for detecting code constructs
34
22
  */
@@ -58,20 +46,24 @@ export class ComplexityAnalyzer {
58
46
  * Analyze complexity of the given lines
59
47
  */
60
48
  analyze(lines, language = 'javascript') {
49
+ const complexityLanguage = this.getComplexityLanguage(language);
61
50
  // Simple cache check based on content hash
62
- const key = this.computeCacheKey(lines);
51
+ const key = `${complexityLanguage}:${this.computeCacheKey(lines)}`;
63
52
  if (key === this.cacheKey && this.cache.has(key)) {
64
53
  return this.cache.get(key);
65
54
  }
66
- const regions = this.identifyRegions(lines, language);
67
- const analyzedRegions = regions.map((region) => this.analyzeRegion(lines, region));
55
+ const tokenized = tokenize(lines.map((line) => line.text).join('\n'), complexityLanguage);
56
+ const regions = this.identifyRegions(lines, complexityLanguage, tokenized);
57
+ const analyzedRegions = regions.map((region) => this.analyzeRegion(lines, region, complexityLanguage, tokenized));
68
58
  const hotspots = this.findHotspots(analyzedRegions);
69
59
  const overall = this.calculateOverall(analyzedRegions, lines.length);
60
+ const totalCognitiveComplexity = analyzedRegions.reduce((total, region) => total + region.cognitiveComplexity, 0);
70
61
  const metrics = {
71
62
  overall,
72
63
  level: this.getLevel(overall),
73
64
  regions: analyzedRegions,
74
- hotspots
65
+ hotspots,
66
+ totalCognitiveComplexity
75
67
  };
76
68
  this.cacheKey = key;
77
69
  this.cache.set(key, metrics);
@@ -113,13 +105,29 @@ export class ComplexityAnalyzer {
113
105
  }
114
106
  return `${lines.length}:${hash}`;
115
107
  }
108
+ getComplexityLanguage(language) {
109
+ const resolved = resolveLanguage(language);
110
+ if (resolved === 'javascript' ||
111
+ resolved === 'typescript' ||
112
+ resolved === 'python' ||
113
+ resolved === 'go') {
114
+ return resolved;
115
+ }
116
+ return 'javascript';
117
+ }
116
118
  /**
117
119
  * Identify code regions (functions, classes, blocks)
118
120
  *
119
121
  * Uses brace-depth tracking to correctly handle inline braces
120
122
  * (object literals, destructuring) that don't represent new blocks.
121
123
  */
122
- identifyRegions(lines, _language) {
124
+ identifyRegions(lines, language, tokenized) {
125
+ if (language === 'python') {
126
+ return this.identifyPythonRegions(lines, tokenized);
127
+ }
128
+ if (language === 'go') {
129
+ return this.identifyGoRegions(lines, tokenized);
130
+ }
123
131
  const regions = [];
124
132
  // Track blocks with their brace depth at push time
125
133
  const blockStack = [];
@@ -223,6 +231,132 @@ export class ComplexityAnalyzer {
223
231
  }
224
232
  return regions;
225
233
  }
234
+ identifyPythonRegions(lines, tokenized) {
235
+ const regions = [];
236
+ const stack = [];
237
+ for (let i = 0; i < lines.length; i++) {
238
+ const tokens = this.getCodeTokens(tokenized[i]?.tokens ?? []);
239
+ if (tokens.length === 0)
240
+ continue;
241
+ const indent = this.getIndent(lines[i].text);
242
+ while (stack.length > 0 && indent <= stack[stack.length - 1].indent) {
243
+ const region = stack.pop();
244
+ regions.push({
245
+ startLine: region.startLine,
246
+ endLine: Math.max(region.startLine, i - 1),
247
+ type: region.type,
248
+ name: region.name
249
+ });
250
+ }
251
+ const first = tokens[0]?.text;
252
+ const second = tokens[1]?.text;
253
+ const isAsyncDef = first === 'async' && second === 'def';
254
+ const isDef = first === 'def' || isAsyncDef;
255
+ const isClass = first === 'class';
256
+ if (isDef || isClass) {
257
+ const keywordIndex = isAsyncDef ? 1 : 0;
258
+ const name = tokens[keywordIndex + 1]?.text;
259
+ stack.push({
260
+ startLine: i,
261
+ indent,
262
+ type: isClass ? 'class' : 'function',
263
+ name
264
+ });
265
+ }
266
+ }
267
+ while (stack.length > 0) {
268
+ const region = stack.pop();
269
+ regions.push({
270
+ startLine: region.startLine,
271
+ endLine: lines.length - 1,
272
+ type: region.type,
273
+ name: region.name
274
+ });
275
+ }
276
+ if (regions.length === 0 && lines.length > 0) {
277
+ regions.push({ startLine: 0, endLine: lines.length - 1, type: 'file' });
278
+ }
279
+ return regions.sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
280
+ }
281
+ identifyGoRegions(lines, tokenized) {
282
+ const regions = [];
283
+ const stack = [];
284
+ let braceDepth = 0;
285
+ let pending;
286
+ for (let i = 0; i < lines.length; i++) {
287
+ const tokens = this.getCodeTokens(tokenized[i]?.tokens ?? []);
288
+ const declaration = this.getGoDeclaration(tokens);
289
+ if (declaration) {
290
+ pending = { line: i, type: declaration.type, name: declaration.name, depth: braceDepth };
291
+ }
292
+ for (const token of tokens) {
293
+ if (token.type === 'punctuation.brace' && token.text === '{') {
294
+ braceDepth++;
295
+ if (pending && braceDepth === pending.depth + 1) {
296
+ stack.push({
297
+ startLine: pending.line,
298
+ depth: braceDepth,
299
+ type: pending.type,
300
+ name: pending.name
301
+ });
302
+ pending = undefined;
303
+ }
304
+ }
305
+ else if (token.type === 'punctuation.brace' && token.text === '}') {
306
+ if (stack.length > 0 && braceDepth === stack[stack.length - 1].depth) {
307
+ const region = stack.pop();
308
+ regions.push({
309
+ startLine: region.startLine,
310
+ endLine: i,
311
+ type: region.type,
312
+ name: region.name
313
+ });
314
+ }
315
+ braceDepth = Math.max(0, braceDepth - 1);
316
+ if (pending && braceDepth < pending.depth) {
317
+ pending = undefined;
318
+ }
319
+ }
320
+ }
321
+ }
322
+ if (regions.length === 0 && lines.length > 0) {
323
+ regions.push({ startLine: 0, endLine: lines.length - 1, type: 'file' });
324
+ }
325
+ return regions.sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
326
+ }
327
+ getGoDeclaration(tokens) {
328
+ const funcIndex = tokens.findIndex((token) => token.text === 'func');
329
+ if (funcIndex !== -1) {
330
+ let nameIndex = funcIndex + 1;
331
+ if (tokens[nameIndex]?.text === '(') {
332
+ let depth = 0;
333
+ for (let i = nameIndex; i < tokens.length; i++) {
334
+ if (tokens[i].text === '(')
335
+ depth++;
336
+ else if (tokens[i].text === ')') {
337
+ depth--;
338
+ if (depth === 0) {
339
+ nameIndex = i + 1;
340
+ break;
341
+ }
342
+ }
343
+ }
344
+ }
345
+ const name = tokens[nameIndex]?.text;
346
+ if (name && /^[A-Za-z_]\w*$/.test(name) && tokens[nameIndex + 1]?.text === '(') {
347
+ return { type: 'function', name };
348
+ }
349
+ }
350
+ const typeIndex = tokens.findIndex((token) => token.text === 'type');
351
+ if (typeIndex !== -1) {
352
+ const name = tokens[typeIndex + 1]?.text;
353
+ const kind = tokens[typeIndex + 2]?.text;
354
+ if (name && (kind === 'struct' || kind === 'interface')) {
355
+ return { type: 'class', name };
356
+ }
357
+ }
358
+ return undefined;
359
+ }
226
360
  /**
227
361
  * Check if a `{` at position `ch` is the opening brace of a function/class definition
228
362
  */
@@ -273,15 +407,19 @@ export class ComplexityAnalyzer {
273
407
  /**
274
408
  * Analyze a specific region
275
409
  */
276
- analyzeRegion(lines, region) {
410
+ analyzeRegion(lines, region, language, tokenized) {
277
411
  const factors = this.calculateFactors(lines, region.startLine, region.endLine);
278
- const score = this.calculateScore(factors);
412
+ const contributions = this.calculateCognitiveContributions(region, language, tokenized);
413
+ const cognitiveComplexity = contributions.reduce((total, contribution) => total + contribution.increment, 0);
414
+ const score = this.calculateScore(cognitiveComplexity);
279
415
  const suggestion = this.getSuggestion(factors, score);
280
416
  return {
281
417
  ...region,
282
418
  score,
283
419
  factors,
284
- suggestion
420
+ suggestion,
421
+ cognitiveComplexity,
422
+ contributions
285
423
  };
286
424
  }
287
425
  /**
@@ -363,16 +501,328 @@ export class ComplexityAnalyzer {
363
501
  callCount
364
502
  };
365
503
  }
504
+ calculateCognitiveContributions(region, language, tokenized) {
505
+ if (language === 'python') {
506
+ return this.calculatePythonCognitiveContributions(region, tokenized);
507
+ }
508
+ return this.calculateBraceCognitiveContributions(region, language, tokenized);
509
+ }
510
+ calculateBraceCognitiveContributions(region, language, tokenized) {
511
+ const contributions = [];
512
+ const nestingStack = [];
513
+ let braceDepth = 0;
514
+ let pendingB2;
515
+ let skipIfAfterElse = false;
516
+ const doWhileDepths = [];
517
+ const isGo = language === 'go';
518
+ for (let lineIndex = region.startLine; lineIndex <= region.endLine; lineIndex++) {
519
+ const tokens = this.getCodeTokens(tokenized[lineIndex]?.tokens ?? []);
520
+ const boolContributions = this.getBooleanSequenceContributions(tokens, lineIndex, nestingStack.length);
521
+ contributions.push(...boolContributions);
522
+ for (let i = 0; i < tokens.length; i++) {
523
+ const token = tokens[i];
524
+ if (token.type === 'punctuation.brace' && token.text === '}') {
525
+ while (nestingStack.length > 0 &&
526
+ nestingStack[nestingStack.length - 1].depth === braceDepth) {
527
+ nestingStack.pop();
528
+ }
529
+ braceDepth = Math.max(0, braceDepth - 1);
530
+ continue;
531
+ }
532
+ if (token.type === 'punctuation.brace' && token.text === '{') {
533
+ braceDepth++;
534
+ if (pendingB2) {
535
+ nestingStack.push({ depth: braceDepth, kind: pendingB2.kind });
536
+ pendingB2 = undefined;
537
+ }
538
+ continue;
539
+ }
540
+ if (token.text === 'else') {
541
+ const next = this.nextNonTextToken(tokens, i + 1);
542
+ if (next?.text === 'if') {
543
+ this.addContribution(contributions, lineIndex, 'else if', 1, nestingStack.length);
544
+ pendingB2 = { kind: 'else if', line: lineIndex };
545
+ skipIfAfterElse = true;
546
+ }
547
+ else {
548
+ this.addContribution(contributions, lineIndex, 'else', 1, nestingStack.length);
549
+ pendingB2 = { kind: 'else', line: lineIndex };
550
+ }
551
+ continue;
552
+ }
553
+ if (token.text === 'if') {
554
+ if (skipIfAfterElse) {
555
+ skipIfAfterElse = false;
556
+ continue;
557
+ }
558
+ this.addContribution(contributions, lineIndex, 'if', 1 + nestingStack.length, nestingStack.length);
559
+ pendingB2 = { kind: 'if', line: lineIndex };
560
+ continue;
561
+ }
562
+ if (token.text === 'for') {
563
+ this.addContribution(contributions, lineIndex, 'for', 1 + nestingStack.length, nestingStack.length);
564
+ pendingB2 = { kind: 'for', line: lineIndex };
565
+ continue;
566
+ }
567
+ if (!isGo && token.text === 'do') {
568
+ this.addContribution(contributions, lineIndex, 'while', 1 + nestingStack.length, nestingStack.length);
569
+ pendingB2 = { kind: 'while', line: lineIndex };
570
+ doWhileDepths.push(braceDepth);
571
+ continue;
572
+ }
573
+ if (!isGo && token.text === 'while') {
574
+ // The `while` that closes a do…while loop belongs to the same loop,
575
+ // already counted at `do` (SonarSource: one increment per loop) — skip
576
+ // it rather than double-count.
577
+ if (doWhileDepths.length > 0 && doWhileDepths[doWhileDepths.length - 1] === braceDepth) {
578
+ doWhileDepths.pop();
579
+ continue;
580
+ }
581
+ this.addContribution(contributions, lineIndex, 'while', 1 + nestingStack.length, nestingStack.length);
582
+ pendingB2 = { kind: 'while', line: lineIndex };
583
+ continue;
584
+ }
585
+ if (token.text === 'switch' ||
586
+ (isGo && (token.text === 'select' || this.isGoTypeSwitch(tokens, i)))) {
587
+ this.addContribution(contributions, lineIndex, 'switch', 1 + nestingStack.length, nestingStack.length);
588
+ pendingB2 = { kind: 'switch', line: lineIndex };
589
+ continue;
590
+ }
591
+ if (!isGo && token.text === 'catch') {
592
+ this.addContribution(contributions, lineIndex, 'catch', 1 + nestingStack.length, nestingStack.length);
593
+ pendingB2 = { kind: 'catch', line: lineIndex };
594
+ continue;
595
+ }
596
+ if (!isGo && token.text === '?') {
597
+ this.addContribution(contributions, lineIndex, 'ternary', 1 + nestingStack.length, nestingStack.length);
598
+ pendingB2 = { kind: 'ternary', line: lineIndex };
599
+ continue;
600
+ }
601
+ if (this.isLabelledJump(tokens, i, isGo)) {
602
+ this.addContribution(contributions, lineIndex, 'labelled-jump', 1, nestingStack.length);
603
+ continue;
604
+ }
605
+ if (this.isDirectRecursiveCall(tokens, i, lineIndex, region)) {
606
+ this.addContribution(contributions, lineIndex, 'recursion', 1, nestingStack.length);
607
+ continue;
608
+ }
609
+ if (this.isNestedFunctionToken(tokens, i, lineIndex, region, language)) {
610
+ this.addContribution(contributions, lineIndex, 'nested-function', 0, nestingStack.length);
611
+ pendingB2 = { kind: 'nested-function', line: lineIndex };
612
+ }
613
+ }
614
+ }
615
+ return contributions;
616
+ }
617
+ calculatePythonCognitiveContributions(region, tokenized) {
618
+ const contributions = [];
619
+ const nestingStack = [];
620
+ for (let lineIndex = region.startLine; lineIndex <= region.endLine; lineIndex++) {
621
+ const line = tokenized[lineIndex];
622
+ const tokens = this.getCodeTokens(line?.tokens ?? []);
623
+ if (tokens.length === 0)
624
+ continue;
625
+ const indent = this.getIndent(line.text);
626
+ while (nestingStack.length > 0 && indent <= nestingStack[nestingStack.length - 1].indent) {
627
+ nestingStack.pop();
628
+ }
629
+ contributions.push(...this.getBooleanSequenceContributions(tokens, lineIndex, nestingStack.length));
630
+ for (let i = 0; i < tokens.length; i++) {
631
+ if (this.isDirectRecursiveCall(tokens, i, lineIndex, region)) {
632
+ this.addContribution(contributions, lineIndex, 'recursion', 1, nestingStack.length);
633
+ }
634
+ }
635
+ const first = tokens[0]?.text;
636
+ const second = tokens[1]?.text;
637
+ const isAsyncDef = first === 'async' && second === 'def';
638
+ const isNestedDef = (first === 'def' || isAsyncDef) && lineIndex !== region.startLine;
639
+ const isNestedClass = first === 'class' && lineIndex !== region.startLine;
640
+ if (isNestedDef || isNestedClass) {
641
+ this.addContribution(contributions, lineIndex, 'nested-function', 0, nestingStack.length);
642
+ nestingStack.push({ indent, kind: 'nested-function' });
643
+ continue;
644
+ }
645
+ if (first === 'if') {
646
+ this.addContribution(contributions, lineIndex, 'if', 1 + nestingStack.length, nestingStack.length);
647
+ nestingStack.push({ indent, kind: 'if' });
648
+ }
649
+ else if (first === 'elif') {
650
+ this.addContribution(contributions, lineIndex, 'else if', 1, nestingStack.length);
651
+ nestingStack.push({ indent, kind: 'else if' });
652
+ }
653
+ else if (first === 'else') {
654
+ this.addContribution(contributions, lineIndex, 'else', 1, nestingStack.length);
655
+ nestingStack.push({ indent, kind: 'else' });
656
+ }
657
+ else if (first === 'for') {
658
+ this.addContribution(contributions, lineIndex, 'for', 1 + nestingStack.length, nestingStack.length);
659
+ nestingStack.push({ indent, kind: 'for' });
660
+ }
661
+ else if (first === 'while') {
662
+ this.addContribution(contributions, lineIndex, 'while', 1 + nestingStack.length, nestingStack.length);
663
+ nestingStack.push({ indent, kind: 'while' });
664
+ }
665
+ else if (first === 'except') {
666
+ this.addContribution(contributions, lineIndex, 'catch', 1 + nestingStack.length, nestingStack.length);
667
+ nestingStack.push({ indent, kind: 'catch' });
668
+ }
669
+ else if (this.isPythonTernary(tokens)) {
670
+ this.addContribution(contributions, lineIndex, 'ternary', 1 + nestingStack.length, nestingStack.length);
671
+ }
672
+ }
673
+ return contributions;
674
+ }
675
+ getCodeTokens(tokens) {
676
+ return tokens.filter((token) => {
677
+ if (token.type === 'text')
678
+ return token.text.trim().length > 0;
679
+ if (token.type === 'comment' || token.type.startsWith('comment.'))
680
+ return false;
681
+ if (token.type === 'string' || token.type.startsWith('string.'))
682
+ return false;
683
+ return true;
684
+ });
685
+ }
686
+ getIndent(text) {
687
+ return text.match(/^[ \t]*/)?.[0].replace(/\t/g, ' ').length ?? 0;
688
+ }
689
+ addContribution(contributions, line, kind, increment, nesting) {
690
+ contributions.push({
691
+ line,
692
+ kind,
693
+ increment,
694
+ nesting,
695
+ reason: `${this.describeContributionKind(kind)} (+${increment}, nesting ${nesting})`
696
+ });
697
+ }
698
+ describeContributionKind(kind) {
699
+ switch (kind) {
700
+ case 'else if':
701
+ return 'else if branch';
702
+ case 'else':
703
+ return 'else branch';
704
+ case 'for':
705
+ return 'for loop';
706
+ case 'while':
707
+ return 'while loop';
708
+ case 'switch':
709
+ return 'switch';
710
+ case 'catch':
711
+ return 'catch clause';
712
+ case 'ternary':
713
+ return 'ternary expression';
714
+ case 'boolean-sequence':
715
+ return 'boolean operator sequence';
716
+ case 'labelled-jump':
717
+ return 'labelled jump';
718
+ case 'recursion':
719
+ return 'recursive call';
720
+ case 'nested-function':
721
+ return 'nested function';
722
+ default:
723
+ return 'if branch';
724
+ }
725
+ }
726
+ getBooleanSequenceContributions(tokens, line, nesting) {
727
+ const contributions = [];
728
+ const lastByParenDepth = new Map();
729
+ let parenDepth = 0;
730
+ for (const token of tokens) {
731
+ if (token.text === '(') {
732
+ parenDepth++;
733
+ continue;
734
+ }
735
+ if (token.text === ')') {
736
+ lastByParenDepth.delete(parenDepth);
737
+ parenDepth = Math.max(0, parenDepth - 1);
738
+ continue;
739
+ }
740
+ if (token.text === ';' || token.text === '{' || token.text === '}' || token.text === ':') {
741
+ lastByParenDepth.clear();
742
+ continue;
743
+ }
744
+ if (token.text !== '&&' &&
745
+ token.text !== '||' &&
746
+ token.text !== 'and' &&
747
+ token.text !== 'or') {
748
+ continue;
749
+ }
750
+ const normalized = token.text === 'and' ? '&&' : token.text === 'or' ? '||' : token.text;
751
+ const previous = lastByParenDepth.get(parenDepth);
752
+ if (previous !== normalized) {
753
+ this.addContribution(contributions, line, 'boolean-sequence', 1, nesting);
754
+ }
755
+ lastByParenDepth.set(parenDepth, normalized);
756
+ }
757
+ return contributions;
758
+ }
759
+ nextNonTextToken(tokens, start) {
760
+ return tokens.slice(start).find((token) => token.text.trim().length > 0);
761
+ }
762
+ isGoTypeSwitch(tokens, index) {
763
+ return (tokens[index].text === 'switch' &&
764
+ tokens.slice(index + 1).some((token) => token.text === 'type'));
765
+ }
766
+ isLabelledJump(tokens, index, isGo) {
767
+ const token = tokens[index];
768
+ if (token.text === 'goto') {
769
+ return !!tokens[index + 1] && /^[A-Za-z_]\w*$/.test(tokens[index + 1].text);
770
+ }
771
+ if (token.text !== 'break' && token.text !== 'continue') {
772
+ return false;
773
+ }
774
+ const next = tokens[index + 1];
775
+ if (!next)
776
+ return false;
777
+ if (isGo) {
778
+ return /^[A-Za-z_]\w*$/.test(next.text);
779
+ }
780
+ return /^[A-Za-z_$][\w$]*$/.test(next.text);
781
+ }
782
+ isNestedFunctionToken(tokens, index, line, region, language) {
783
+ const token = tokens[index];
784
+ if (token.text === '=>') {
785
+ return line !== region.startLine;
786
+ }
787
+ if (token.text !== 'function' && token.text !== 'func') {
788
+ return false;
789
+ }
790
+ if (line === region.startLine) {
791
+ return false;
792
+ }
793
+ if (language === 'go') {
794
+ return true;
795
+ }
796
+ return token.text === 'function';
797
+ }
798
+ isDirectRecursiveCall(tokens, index, line, region) {
799
+ if (!region.name || tokens[index].text !== region.name || tokens[index + 1]?.text !== '(') {
800
+ return false;
801
+ }
802
+ const previous = tokens[index - 1]?.text;
803
+ if (previous === 'function' || previous === 'func' || previous === 'def') {
804
+ return false;
805
+ }
806
+ if (line === region.startLine &&
807
+ previous === ')' &&
808
+ tokens.slice(0, index).some((t) => t.text === 'func')) {
809
+ return false;
810
+ }
811
+ return true;
812
+ }
813
+ isPythonTernary(tokens) {
814
+ if (tokens[0]?.text === 'if')
815
+ return false;
816
+ const ifIndex = tokens.findIndex((token) => token.text === 'if');
817
+ if (ifIndex <= 0)
818
+ return false;
819
+ return tokens.slice(ifIndex + 1).some((token) => token.text === 'else');
820
+ }
366
821
  /**
367
822
  * Calculate complexity score from factors
368
823
  */
369
- calculateScore(factors) {
370
- const score = factors.nestingDepth * WEIGHTS.nestingDepth +
371
- factors.branchingFactor * WEIGHTS.branchingFactor +
372
- factors.lineCount * WEIGHTS.lineCount +
373
- factors.identifierCount * WEIGHTS.identifierCount +
374
- factors.callCount * WEIGHTS.callCount;
375
- return Math.min(100, Math.round(score));
824
+ calculateScore(cognitiveComplexity) {
825
+ return Math.min(100, Math.round(cognitiveComplexity * COGNITIVE_SCORE_MULTIPLIER));
376
826
  }
377
827
  /**
378
828
  * Get suggestion based on factors
@@ -6,6 +6,27 @@
6
6
  * and predicts potential merge conflicts before they happen.
7
7
  */
8
8
  import type { SemanticRegion } from './semantic-analyzer';
9
+ export interface AwarenessCursorUser {
10
+ id: string;
11
+ name: string;
12
+ color: string;
13
+ isAI?: boolean;
14
+ }
15
+ export interface AwarenessCursorSource {
16
+ getCursors(): Map<number, {
17
+ user: AwarenessCursorUser;
18
+ cursor: {
19
+ anchor: number;
20
+ head: number;
21
+ };
22
+ }>;
23
+ }
24
+ export interface AwarenessPositionBinding {
25
+ indexToPosition(index: number): {
26
+ line: number;
27
+ column: number;
28
+ };
29
+ }
9
30
  /**
10
31
  * User awareness state for conflict detection
11
32
  */
@@ -68,6 +89,17 @@ export interface ConflictPredictorConfig {
68
89
  /** Enable AI conflict detection (default: true) */
69
90
  includeAI: boolean;
70
91
  }
92
+ /**
93
+ * Mark a real awareness user's edit activity.
94
+ *
95
+ * Cursor changes are detected by awarenessToUserAwareness automatically. Use this
96
+ * when an editor content change happens without a distinct cursor update.
97
+ */
98
+ export declare function markAwarenessActivity(awareness: object, userId: string, now: number, recentlyEditedLines?: number[]): void;
99
+ /**
100
+ * Convert live Yjs awareness cursors into ConflictPredictor input.
101
+ */
102
+ export declare function awarenessToUserAwareness(awareness: AwarenessCursorSource, binding: AwarenessPositionBinding, now: number): UserAwareness[];
71
103
  /**
72
104
  * Conflict Predictor class
73
105
  */
@@ -5,12 +5,67 @@
5
5
  * Detects when multiple users are editing the same semantic region
6
6
  * and predicts potential merge conflicts before they happen.
7
7
  */
8
+ const awarenessActivity = new WeakMap();
8
9
  const DEFAULT_CONFIG = {
9
10
  recentEditWindow: 30000,
10
11
  proximityThreshold: 10,
11
12
  warningThreshold: 0.3,
12
13
  includeAI: true
13
14
  };
15
+ function getActivityRecords(source) {
16
+ let records = awarenessActivity.get(source);
17
+ if (!records) {
18
+ records = new Map();
19
+ awarenessActivity.set(source, records);
20
+ }
21
+ return records;
22
+ }
23
+ /**
24
+ * Mark a real awareness user's edit activity.
25
+ *
26
+ * Cursor changes are detected by awarenessToUserAwareness automatically. Use this
27
+ * when an editor content change happens without a distinct cursor update.
28
+ */
29
+ export function markAwarenessActivity(awareness, userId, now, recentlyEditedLines = []) {
30
+ const records = getActivityRecords(awareness);
31
+ const existing = records.get(userId);
32
+ records.set(userId, {
33
+ cursorSignature: existing?.cursorSignature ?? '',
34
+ lastEditTime: now,
35
+ recentlyEditedLines
36
+ });
37
+ }
38
+ /**
39
+ * Convert live Yjs awareness cursors into ConflictPredictor input.
40
+ */
41
+ export function awarenessToUserAwareness(awareness, binding, now) {
42
+ const records = getActivityRecords(awareness);
43
+ const users = [];
44
+ for (const [, { user, cursor }] of awareness.getCursors()) {
45
+ const position = binding.indexToPosition(cursor.head);
46
+ const signature = `${cursor.anchor}:${cursor.head}`;
47
+ const existing = records.get(user.id);
48
+ const changed = !existing || existing.cursorSignature !== signature;
49
+ const lastEditTime = changed ? now : existing.lastEditTime;
50
+ const recentlyEditedLines = changed ? [position.line] : existing.recentlyEditedLines;
51
+ records.set(user.id, {
52
+ cursorSignature: signature,
53
+ lastEditTime,
54
+ recentlyEditedLines
55
+ });
56
+ users.push({
57
+ id: user.id,
58
+ name: user.name,
59
+ color: user.color,
60
+ isAI: user.isAI ?? false,
61
+ cursorLine: position.line,
62
+ cursorColumn: position.column,
63
+ lastEditTime,
64
+ recentlyEditedLines
65
+ });
66
+ }
67
+ return users;
68
+ }
14
69
  /**
15
70
  * Conflict Predictor class
16
71
  */