@nocturnium/svelte-ide 1.1.0 → 1.2.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.
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 +345 -110
  5. package/dist/components/editor/ComplexityLayer.svelte.d.ts +20 -0
  6. package/dist/components/editor/ConflictZoneLayer.svelte +22 -15
  7. package/dist/components/editor/CustomEditor.svelte +81 -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
@@ -20,11 +20,16 @@
20
20
  import { createEditorInput } from './editor-input';
21
21
  import { createEditorScroll } from './editor-scroll';
22
22
  import ComplexityLayer from './ComplexityLayer.svelte';
23
+ import ComplexityHeatLayer from './ComplexityHeatLayer.svelte';
23
24
  import AIFocusLayer from './AIFocusLayer.svelte';
24
25
  import EditorSelections from './EditorSelections.svelte';
25
26
  import EditorLines from './EditorLines.svelte';
26
27
  import CommandPalette from './CommandPalette.svelte';
27
- import { getComplexityAnalyzer, type ComplexityMetrics } from './core/complexity-analyzer';
28
+ import {
29
+ getComplexityAnalyzer,
30
+ type ComplexityMetrics,
31
+ type ComplexityRegion
32
+ } from './core/complexity-analyzer';
28
33
  import { registerSemanticFoldCommands } from './core/commands';
29
34
  import { getSemanticAnalyzer, type FoldPreset } from './core/semantic-analyzer';
30
35
  import type { AIAwareness } from './core/ai-awareness';
@@ -127,6 +132,8 @@
127
132
  const complexityAnalyzer = getComplexityAnalyzer();
128
133
  let complexityMetrics = $state<ComplexityMetrics | null>(null);
129
134
  let complexityUpdateTimeout: ReturnType<typeof setTimeout> | null = null;
135
+ let complexityFlashTimeout: ReturnType<typeof setTimeout> | null = null;
136
+ let complexityFlashRegionKey = $state('');
130
137
 
131
138
  // DOM refs
132
139
  let container: HTMLDivElement;
@@ -148,6 +155,7 @@
148
155
  // Reactive viewport height of the scrollable content area. Drives line
149
156
  // virtualization (only rows within scrollTop +/- viewport + overscan render).
150
157
  let viewportHeight = $state(FALLBACK_VIEWPORT_HEIGHT);
158
+ let viewportWidth = $state(0);
151
159
 
152
160
  // Selection rendering
153
161
  let cursorVisible = $state(true);
@@ -545,6 +553,43 @@
545
553
  foldManager.expandAll();
546
554
  }
547
555
 
556
+ function getComplexityRegionKey(region: ComplexityRegion): string {
557
+ return `${region.startLine}:${region.endLine}:${region.name ?? region.type}:${region.score}`;
558
+ }
559
+
560
+ export function flashComplexityRegion(region: ComplexityRegion) {
561
+ if (complexityFlashTimeout) {
562
+ clearTimeout(complexityFlashTimeout);
563
+ }
564
+
565
+ complexityFlashRegionKey = '';
566
+ requestAnimationFrame(() => {
567
+ complexityFlashRegionKey = getComplexityRegionKey(region);
568
+ complexityFlashTimeout = setTimeout(() => {
569
+ complexityFlashRegionKey = '';
570
+ complexityFlashTimeout = null;
571
+ }, 1000);
572
+ });
573
+ }
574
+
575
+ /**
576
+ * Imperative API (accessible via `bind:this`): scroll a raw document line into
577
+ * view and optionally flash the matching complexity region.
578
+ */
579
+ export async function scrollToLine(line: number, region?: ComplexityRegion) {
580
+ await tick();
581
+ if (!editorContent) return;
582
+
583
+ const row = lineToVisualRow(line);
584
+ const targetTop = row * lineHeight;
585
+ editorContent.scrollTop = Math.max(0, targetTop - editorContent.clientHeight * 0.3);
586
+
587
+ if (region) {
588
+ flashComplexityRegion(region);
589
+ }
590
+ hiddenInput?.focus();
591
+ }
592
+
548
593
  function handleFoldIndicatorClick(lineNumber: number, e: MouseEvent) {
549
594
  e.preventDefault();
550
595
  e.stopPropagation();
@@ -579,6 +624,16 @@
579
624
  // Total scrollable content height (in px) for the full document, accounting
580
625
  // for folded/hidden lines. Drives the spacer so the scrollbar stays correct.
581
626
  let totalContentHeight = $derived(Math.max(visibleLines.length, 1) * lineHeight);
627
+ let estimatedContentWidth = $derived.by(() => {
628
+ let maxLineLength = 0;
629
+ for (const line of lines) {
630
+ maxLineLength = Math.max(maxLineLength, line?.text.length ?? 0);
631
+ }
632
+ return Math.max(
633
+ viewportWidth,
634
+ gutterWidth + CONTENT_PADDING + maxLineLength * charWidth + CONTENT_PADDING
635
+ );
636
+ });
582
637
 
583
638
  // Windowed slice of visibleLines: only the rows intersecting the viewport
584
639
  // (plus overscan) are rendered to the DOM. Each entry keeps its absolute
@@ -768,6 +823,9 @@
768
823
  $effect(() => {
769
824
  if (editorState && language !== editorState.language) {
770
825
  editorState.setLanguage(language);
826
+ if (complexityHighlighting) {
827
+ updateComplexityMetrics();
828
+ }
771
829
  }
772
830
  });
773
831
 
@@ -813,6 +871,7 @@
813
871
  // Measure the initial viewport height for line virtualization.
814
872
  if (editorContent) {
815
873
  viewportHeight = editorContent.clientHeight || FALLBACK_VIEWPORT_HEIGHT;
874
+ viewportWidth = editorContent.clientWidth || 0;
816
875
  }
817
876
 
818
877
  // Register semantic fold commands for the command palette
@@ -845,6 +904,7 @@
845
904
  inputHandlers.measureCharacter();
846
905
  if (editorContent) {
847
906
  viewportHeight = editorContent.clientHeight || FALLBACK_VIEWPORT_HEIGHT;
907
+ viewportWidth = editorContent.clientWidth || 0;
848
908
  }
849
909
  });
850
910
  if (container) {
@@ -892,6 +952,11 @@
892
952
  complexityUpdateTimeout = null;
893
953
  }
894
954
 
955
+ if (complexityFlashTimeout) {
956
+ clearTimeout(complexityFlashTimeout);
957
+ complexityFlashTimeout = null;
958
+ }
959
+
895
960
  // Clean up semantic fold command registration
896
961
  unregisterSemanticFoldCommands?.();
897
962
  unregisterSemanticFoldCommands = null;
@@ -1001,12 +1066,27 @@
1001
1066
  >
1002
1067
  <!-- Complexity highlighting layer (bottommost) -->
1003
1068
  {#if complexityHighlighting && complexityMetrics}
1069
+ <ComplexityHeatLayer
1070
+ metrics={complexityMetrics}
1071
+ {lineHeight}
1072
+ {gutterWidth}
1073
+ totalHeight={totalContentHeight}
1074
+ contentWidth={estimatedContentWidth}
1075
+ minScore={complexityThreshold}
1076
+ enabled={complexityHighlighting}
1077
+ flashRegionKey={complexityFlashRegionKey}
1078
+ lineToVisualRow={(line) => lineToVisualRow(line)}
1079
+ />
1004
1080
  <ComplexityLayer
1005
1081
  metrics={complexityMetrics}
1006
1082
  {lineHeight}
1007
1083
  {gutterWidth}
1084
+ totalHeight={totalContentHeight}
1085
+ contentWidth={estimatedContentWidth}
1008
1086
  minScore={complexityThreshold}
1009
1087
  enabled={complexityHighlighting}
1088
+ flashRegionKey={complexityFlashRegionKey}
1089
+ lineToVisualRow={(line) => lineToVisualRow(line)}
1010
1090
  />
1011
1091
  {/if}
1012
1092
 
@@ -1,6 +1,6 @@
1
1
  import type { EditorPreferences } from '../../types';
2
2
  import { type Cursor } from './core';
3
- import { type ComplexityMetrics } from './core/complexity-analyzer';
3
+ import { type ComplexityMetrics, type ComplexityRegion } from './core/complexity-analyzer';
4
4
  import { type FoldPreset } from './core/semantic-analyzer';
5
5
  import type { AIAwareness } from './core/ai-awareness';
6
6
  interface Props {
@@ -36,6 +36,8 @@ interface Props {
36
36
  declare const CustomEditor: import("svelte").Component<Props, {
37
37
  applyFoldPreset: (preset: FoldPreset) => void;
38
38
  unfoldAll: () => void;
39
+ flashComplexityRegion: (region: ComplexityRegion) => void;
40
+ scrollToLine: (line: number, region?: ComplexityRegion) => Promise<void>;
39
41
  }, "content">;
40
42
  type CustomEditor = ReturnType<typeof CustomEditor>;
41
43
  export default CustomEditor;
@@ -37,19 +37,23 @@
37
37
  let echoCursors = $state<EchoCursor[]>([]);
38
38
  let replayingCursors = $state<Set<string>>(new Set());
39
39
  let recentReplay = new SvelteMap<string, { text: string; opacity: number }>();
40
+ let echoContents = $state<Record<string, string>>({});
40
41
 
41
42
  // Subscribe to echo cursor events
42
43
  onMount(() => {
43
44
  // Initialize with current cursors
44
45
  echoCursors = manager.getEchoCursors();
46
+ echoContents = getEchoContents();
45
47
 
46
48
  const unsubscribe = manager.subscribe((event: EchoCursorEvent) => {
47
49
  switch (event.type) {
48
50
  case 'echo-added':
49
51
  echoCursors = manager.getEchoCursors();
52
+ echoContents = getEchoContents();
50
53
  break;
51
54
  case 'echo-removed':
52
55
  echoCursors = manager.getEchoCursors();
56
+ echoContents = getEchoContents();
53
57
  break;
54
58
  case 'replay-started':
55
59
  replayingCursors = new Set([...replayingCursors, event.cursorId]);
@@ -63,6 +67,8 @@
63
67
  break;
64
68
  case 'replay-completed':
65
69
  replayingCursors = new Set([...replayingCursors].filter((id) => id !== event.cursorId));
70
+ echoCursors = manager.getEchoCursors();
71
+ echoContents = getEchoContents();
66
72
  // Fade out replay text
67
73
  setTimeout(() => {
68
74
  const entry = recentReplay.get(event.cursorId);
@@ -78,6 +84,7 @@
78
84
  if (!event.enabled) {
79
85
  echoCursors = [];
80
86
  replayingCursors = new Set();
87
+ echoContents = {};
81
88
  recentReplay.clear();
82
89
  }
83
90
  break;
@@ -113,6 +120,12 @@
113
120
  return tokens[Math.max(index, 0) % tokens.length];
114
121
  }
115
122
 
123
+ function getEchoContents(): Record<string, string> {
124
+ return Object.fromEntries(
125
+ manager.getEchoCursors().map((cursor) => [cursor.id, manager.getEchoContent(cursor.id)])
126
+ );
127
+ }
128
+
116
129
  /**
117
130
  * Handle remove echo click
118
131
  */
@@ -127,6 +140,7 @@
127
140
  {#each echoCursors as cursor (cursor.id)}
128
141
  {@const isReplaying = replayingCursors.has(cursor.id)}
129
142
  {@const replayText = recentReplay.get(cursor.id)}
143
+ {@const echoContent = echoContents[cursor.id] ?? ''}
130
144
 
131
145
  <!-- Echo cursor marker -->
132
146
  <div
@@ -171,6 +185,21 @@
171
185
  <div class="echo-cursor__ripple"></div>
172
186
  {/if}
173
187
  </div>
188
+
189
+ <!-- Echo readout: the mirror's CURRENT line at the echo position, drawn on
190
+ an opaque lane so it never composites over the editor's own code
191
+ glyphs (a full-document transparent overlay read as doubled text). -->
192
+ <div
193
+ class="echo-cursor-buffer"
194
+ style="top: {cursor.position.line *
195
+ lineHeight}px; left: {gutterWidth}px; height: {lineHeight}px; --echo-color: var({getEchoColorToken(
196
+ cursor
197
+ )});"
198
+ >
199
+ <span class="echo-cursor-buffer__text"
200
+ >{echoContent.split('\n')[cursor.position.line] || ' '}</span
201
+ >
202
+ </div>
174
203
  {/each}
175
204
 
176
205
  <!-- Mode indicator -->
@@ -199,6 +228,8 @@
199
228
  .echo-cursor {
200
229
  position: absolute;
201
230
  pointer-events: auto;
231
+ /* Keep the caret/label/replay above the echo readout lane. */
232
+ z-index: 1;
202
233
  }
203
234
 
204
235
  .echo-cursor__caret {
@@ -327,6 +358,35 @@
327
358
  animation: echo-ripple 0.4s ease-out forwards;
328
359
  }
329
360
 
361
+ .echo-cursor-buffer {
362
+ position: absolute;
363
+ right: 0;
364
+ display: flex;
365
+ align-items: center;
366
+ padding: 0 8px 0 10px;
367
+ font-family: monospace;
368
+ font-size: 13px;
369
+ line-height: 1;
370
+ white-space: pre;
371
+ overflow: hidden;
372
+ pointer-events: none;
373
+ /* Opaque lane: the echoed line must never show through onto the editor's
374
+ own glyphs — that reads as doubled, illegible code. */
375
+ background: linear-gradient(
376
+ 90deg,
377
+ color-mix(in srgb, var(--echo-color) 24%, var(--ide-bg-secondary, #1a2744)) 0%,
378
+ var(--ide-bg-secondary, #1a2744) 72%
379
+ );
380
+ border-left: 2px solid var(--echo-color);
381
+ box-shadow: 0 1px 4px color-mix(in srgb, #000 35%, transparent);
382
+ }
383
+
384
+ .echo-cursor-buffer__text {
385
+ overflow: hidden;
386
+ text-overflow: ellipsis;
387
+ color: color-mix(in srgb, var(--echo-color) 60%, var(--ide-text-primary));
388
+ }
389
+
330
390
  @keyframes echo-ripple {
331
391
  0% {
332
392
  transform: scale(0.5);
@@ -1,10 +1,10 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * Plugin Preview Sandbox
3
+ * Plugin Preview
4
4
  *
5
- * A sandboxed environment for previewing plugin effects
6
- * before applying them to the main editor. Supports live
7
- * code transformation preview with rollback capability.
5
+ * Preview plugin effects before applying them to the main editor.
6
+ * Plugin transform functions are trusted caller-provided code and run
7
+ * in the host realm, not in an isolated sandbox.
8
8
  */
9
9
 
10
10
  interface PluginTransform {
@@ -71,6 +71,9 @@
71
71
  let isProcessing = $state(false);
72
72
  let previewMode = $state<'split' | 'unified' | 'diff'>('split');
73
73
  let sandboxContainer = $state<HTMLDivElement>(null!);
74
+ let transformRunId = 0;
75
+
76
+ const TRANSFORM_TIMEOUT_MS = 2_000;
74
77
 
75
78
  // Demo plugins for showcase
76
79
  const demoPlugins: PluginDefinition[] = [
@@ -164,18 +167,20 @@
164
167
  const activePlugins = $derived(plugins.length > 0 ? plugins : demoPlugins);
165
168
 
166
169
  /**
167
- * Run plugin transform
170
+ * Run trusted plugin transform with an async watchdog.
168
171
  */
169
172
  async function runTransform(plugin: PluginDefinition) {
173
+ const runId = ++transformRunId;
170
174
  selectedPlugin = plugin;
171
175
  isProcessing = true;
172
176
 
173
177
  const startTime = performance.now();
174
178
 
175
179
  try {
176
- const result = await Promise.resolve(plugin.transform(code));
180
+ const result = await runTrustedTransform(plugin, code);
177
181
  const executionTime = performance.now() - startTime;
178
182
 
183
+ if (runId !== transformRunId) return;
179
184
  currentTransform = {
180
185
  id: `${plugin.id}-${Date.now()}`,
181
186
  pluginName: plugin.name,
@@ -187,6 +192,7 @@
187
192
  executionTime
188
193
  };
189
194
  } catch (error) {
195
+ if (runId !== transformRunId) return;
190
196
  currentTransform = {
191
197
  id: `${plugin.id}-${Date.now()}`,
192
198
  pluginName: plugin.name,
@@ -198,9 +204,35 @@
198
204
  error: error instanceof Error ? error.message : 'Unknown error',
199
205
  executionTime: performance.now() - startTime
200
206
  };
207
+ } finally {
208
+ if (runId === transformRunId) {
209
+ isProcessing = false;
210
+ }
201
211
  }
212
+ }
202
213
 
203
- isProcessing = false;
214
+ function runTrustedTransform(plugin: PluginDefinition, input: string): Promise<string> {
215
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
216
+ const timeout = new Promise<never>((_, reject) => {
217
+ timeoutId = setTimeout(() => {
218
+ reject(new Error(`Transform timed out after ${TRANSFORM_TIMEOUT_MS}ms`));
219
+ }, TRANSFORM_TIMEOUT_MS);
220
+ });
221
+
222
+ const transform = Promise.resolve()
223
+ .then(() => plugin.transform(input))
224
+ .then((result) => {
225
+ if (typeof result !== 'string') {
226
+ throw new Error('Transform returned a non-string result');
227
+ }
228
+ return result;
229
+ });
230
+
231
+ return Promise.race([transform, timeout]).finally(() => {
232
+ if (timeoutId !== undefined) {
233
+ clearTimeout(timeoutId);
234
+ }
235
+ });
204
236
  }
205
237
 
206
238
  /**
@@ -246,8 +278,10 @@
246
278
  * Reset to original
247
279
  */
248
280
  function resetTransform() {
281
+ transformRunId++;
249
282
  currentTransform = null;
250
283
  selectedPlugin = null;
284
+ isProcessing = false;
251
285
  }
252
286
 
253
287
  /**
@@ -273,7 +307,7 @@
273
307
  {#if visible}
274
308
  <div class="plugin-sandbox" bind:this={sandboxContainer}>
275
309
  <div class="plugin-sandbox__header">
276
- <h3 class="plugin-sandbox__title">Plugin Preview Sandbox</h3>
310
+ <h3 class="plugin-sandbox__title">Plugin Preview</h3>
277
311
  <div class="plugin-sandbox__actions">
278
312
  <div class="preview-mode-toggle">
279
313
  <button
@@ -387,7 +421,7 @@
387
421
  <div class="preview-empty">
388
422
  <p>Select a plugin to preview its transformation</p>
389
423
  <p class="preview-hint">
390
- Your code will be transformed in this sandbox before applying
424
+ Trusted plugin code runs in this editor before applying changes
391
425
  </p>
392
426
  </div>
393
427
  {/if}
@@ -11,11 +11,11 @@ interface PluginDefinition {
11
11
  transform: (code: string) => string | Promise<string>;
12
12
  }
13
13
  /**
14
- * Plugin Preview Sandbox
14
+ * Plugin Preview
15
15
  *
16
- * A sandboxed environment for previewing plugin effects
17
- * before applying them to the main editor. Supports live
18
- * code transformation preview with rollback capability.
16
+ * Preview plugin effects before applying them to the main editor.
17
+ * Plugin transform functions are trusted caller-provided code and run
18
+ * in the host realm, not in an isolated sandbox.
19
19
  */
20
20
  interface PluginTransform {
21
21
  /** Transform ID */
@@ -22,6 +22,13 @@ export interface ComplexityFactors {
22
22
  /** Number of function calls */
23
23
  callCount: number;
24
24
  }
25
+ export interface ComplexityContribution {
26
+ line: number;
27
+ kind: string;
28
+ reason: string;
29
+ increment: number;
30
+ nesting: number;
31
+ }
25
32
  /**
26
33
  * A region of code with its complexity analysis
27
34
  */
@@ -40,6 +47,10 @@ export interface ComplexityRegion {
40
47
  type: 'function' | 'class' | 'block' | 'file';
41
48
  /** Region name if identifiable */
42
49
  name?: string;
50
+ /** Exact SonarSource Cognitive Complexity score */
51
+ cognitiveComplexity: number;
52
+ /** Per-increment Cognitive Complexity contribution breakdown */
53
+ contributions: ComplexityContribution[];
43
54
  }
44
55
  /**
45
56
  * Overall complexity metrics for a document
@@ -53,6 +64,8 @@ export interface ComplexityMetrics {
53
64
  regions: ComplexityRegion[];
54
65
  /** Lines that exceed threshold */
55
66
  hotspots: number[];
67
+ /** Sum of exact Cognitive Complexity across all regions */
68
+ totalCognitiveComplexity: number;
56
69
  }
57
70
  /**
58
71
  * Complexity Analyzer class
@@ -76,6 +89,7 @@ export declare class ComplexityAnalyzer {
76
89
  * Compute a cache key from all line content using a fast hash
77
90
  */
78
91
  private computeCacheKey;
92
+ private getComplexityLanguage;
79
93
  /**
80
94
  * Identify code regions (functions, classes, blocks)
81
95
  *
@@ -83,6 +97,9 @@ export declare class ComplexityAnalyzer {
83
97
  * (object literals, destructuring) that don't represent new blocks.
84
98
  */
85
99
  private identifyRegions;
100
+ private identifyPythonRegions;
101
+ private identifyGoRegions;
102
+ private getGoDeclaration;
86
103
  /**
87
104
  * Check if a `{` at position `ch` is the opening brace of a function/class definition
88
105
  */
@@ -99,6 +116,20 @@ export declare class ComplexityAnalyzer {
99
116
  * Calculate complexity factors for a range of lines
100
117
  */
101
118
  private calculateFactors;
119
+ private calculateCognitiveContributions;
120
+ private calculateBraceCognitiveContributions;
121
+ private calculatePythonCognitiveContributions;
122
+ private getCodeTokens;
123
+ private getIndent;
124
+ private addContribution;
125
+ private describeContributionKind;
126
+ private getBooleanSequenceContributions;
127
+ private nextNonTextToken;
128
+ private isGoTypeSwitch;
129
+ private isLabelledJump;
130
+ private isNestedFunctionToken;
131
+ private isDirectRecursiveCall;
132
+ private isPythonTernary;
102
133
  /**
103
134
  * Calculate complexity score from factors
104
135
  */