ide-assi 0.402.0 → 0.403.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.
@@ -1,6 +1,6 @@
1
1
  import ninegrid from "ninegrid2";
2
2
 
3
- // CodeMirror 6 핵심 및 확장 임포트 (변동 없음)
3
+ // CodeMirror 6 핵심 및 확장 임포트
4
4
  import {
5
5
  EditorView, lineNumbers, highlightSpecialChars, drawSelection,
6
6
  dropCursor, keymap, highlightActiveLine, highlightActiveLineGutter,
@@ -12,47 +12,57 @@ import {
12
12
  StateEffect,
13
13
  Text
14
14
  } from "@codemirror/state";
15
- import { history, historyKeymap, indentWithTab } from "@codemirror/commands";
16
- import { defaultKeymap, selectAll } from "@codemirror/commands";
15
+ import { history, historyKeymap, indentWithTab, selectAll } from "@codemirror/commands"; // selectAll, indentWithTab 임포트 확인
16
+ import { defaultKeymap } from "@codemirror/commands"; // defaultKeymap 별도 임포트
17
17
  import { searchKeymap, highlightSelectionMatches } from "@codemirror/search";
18
- import { bracketMatching } from "@codemirror/language";
19
- import { javascript } from "@codemirror/lang-javascript";
20
- import { indentOnInput, syntaxHighlighting, defaultHighlightStyle } from "@codemirror/language";
18
+ import { bracketMatching, syntaxHighlighting, defaultHighlightStyle } from "@codemirror/language";
19
+ import { javascript } from "@codemirror/lang-javascript"; // 언어 확장 임포트
20
+
21
21
  import { lintKeymap } from "@codemirror/lint";
22
22
  import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
23
23
 
24
- // Diff 로직을 위해 diff-match-patch 사용 (변동 없음)
24
+ // Diff 로직을 위해 diff-match-patch 사용
25
25
  import { diff_match_patch } from 'diff-match-patch';
26
26
 
27
- // Diff 데코레이션을 위한 StateField 정의 (변동 없음)
27
+ // --- Diff 데코레이션을 위한 StateEffect 정의 (수정: DecorationSet 타입 명시) ---
28
+ const setAsisDecorationsEffect = StateEffect.define();
29
+ const setTobeDecorationsEffect = StateEffect.define();
30
+
31
+ // --- Diff 데코레이션을 위한 StateField 정의 ---
28
32
  const asisDiffDecorations = StateField.define({
29
- create() { return Decoration.none; },
33
+ create() {
34
+ return Decoration.none; // 초기 상태: 데코레이션 없음
35
+ },
30
36
  update(decorations, tr) {
31
- return tr.effects.reduce((currentDecos, effect) => {
37
+ // 트랜잭션의 변화를 데코레이션에 매핑하여 위치가 변경되어도 데코레이션이 유지되도록 함
38
+ decorations = decorations.map(tr.changes);
39
+ // setAsisDecorationsEffect가 있다면 해당 효과의 값을 적용
40
+ for (let effect of tr.effects) {
32
41
  if (effect.is(setAsisDecorationsEffect)) {
33
- return effect.value;
42
+ return effect.value; // 새로운 DecorationSet으로 교체
34
43
  }
35
- return currentDecos;
36
- }, decorations);
44
+ }
45
+ return decorations; // 효과가 없으면 기존 데코레이션 반환
37
46
  },
38
- provide: f => EditorView.decorations.from(f)
47
+ provide: f => EditorView.decorations.from(f) // 이 필드가 제공하는 데코레이션을 EditorView.decorations 확장으로 등록
39
48
  });
40
49
 
41
50
  const tobeDiffDecorations = StateField.define({
42
- create() { return Decoration.none; },
51
+ create() {
52
+ return Decoration.none; // 초기 상태: 데코레이션 없음
53
+ },
43
54
  update(decorations, tr) {
44
- return tr.effects.reduce((currentDecos, effect) => {
55
+ decorations = decorations.map(tr.changes);
56
+ for (let effect of tr.effects) {
45
57
  if (effect.is(setTobeDecorationsEffect)) {
46
- return effect.value;
58
+ return effect.value; // 새로운 DecorationSet으로 교체
47
59
  }
48
- return currentDecos;
49
- }, decorations);
60
+ }
61
+ return decorations;
50
62
  },
51
63
  provide: f => EditorView.decorations.from(f)
52
64
  });
53
65
 
54
- const setAsisDecorationsEffect = StateEffect.define();
55
- const setTobeDecorationsEffect = StateEffect.define();
56
66
 
57
67
  export class IdeDiff extends HTMLElement {
58
68
 
@@ -76,9 +86,13 @@ export class IdeDiff extends HTMLElement {
76
86
  connectedCallback() {
77
87
  this.shadowRoot.innerHTML = `
78
88
  <style>
79
- /* ninegrid CSS 및 필요한 기본 스타일 (변동 없음) */
89
+ /* ninegrid CSS 및 필요한 기본 스타일 */
80
90
  @import "https://cdn.jsdelivr.net/npm/ninegrid@${ninegrid.version}/dist/css/ideDiff.css";
81
91
  ${ninegrid.getCustomPath(this, "ideDiff.css")}
92
+
93
+ /* Diff 데코레이션 CSS 직접 추가 (없으면 ideDiff.css에 추가) */
94
+ .cm-inserted-line-bg { background-color: #e6ffed; border-left: 3px solid #66bb6a; }
95
+ .cm-deleted-line-bg { background-color: #ffebe9; border-left: 3px solid #ef5350; }
82
96
  </style>
83
97
 
84
98
  <div class="wrapper">
@@ -93,6 +107,22 @@ export class IdeDiff extends HTMLElement {
93
107
  });
94
108
  }
95
109
 
110
+ disconnectedCallback() {
111
+ // 컴포넌트 해제 시 스크롤 리스너 제거
112
+ if (this._asisScrollHandler) {
113
+ this.#asisEditorView.scrollDOM.removeEventListener('scroll', this._asisScrollHandler);
114
+ this.#tobeEditorView.scrollDOM.removeEventListener('scroll', this._tobeScrollHandler);
115
+ this._asisScrollHandler = null;
116
+ this._tobeScrollHandler = null;
117
+ }
118
+ if (this.#asisEditorView) {
119
+ this.#asisEditorView.destroy();
120
+ }
121
+ if (this.#tobeEditorView) {
122
+ this.#tobeEditorView.destroy();
123
+ }
124
+ }
125
+
96
126
  #initCodeMirror = () => {
97
127
  this.#asisEditorEl = this.shadowRoot.querySelector('.panel.asis');
98
128
  this.#tobeEditorEl = this.shadowRoot.querySelector('.panel.tobe');
@@ -109,9 +139,10 @@ export class IdeDiff extends HTMLElement {
109
139
  drawSelection(),
110
140
  dropCursor(),
111
141
  EditorState.allowMultipleSelections.of(true),
112
- indentOnInput(),
142
+ indentOnInput(), // 함수 호출
113
143
  bracketMatching(),
114
144
  highlightActiveLine(),
145
+ highlightActiveLineGutter(),
115
146
  highlightSelectionMatches(),
116
147
  keymap.of([
117
148
  ...defaultKeymap,
@@ -119,11 +150,11 @@ export class IdeDiff extends HTMLElement {
119
150
  ...historyKeymap,
120
151
  ...lintKeymap,
121
152
  ...completionKeymap,
122
- indentWithTab,
123
- selectAll
153
+ indentWithTab(), // 함수 호출
154
+ selectAll() // 함수 호출
124
155
  ]),
125
- syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
126
- autocompletion(),
156
+ syntaxHighlighting(defaultHighlightStyle), // { fallback: true } 제거
157
+ autocompletion(), // 함수 호출
127
158
  ];
128
159
 
129
160
  // ASIS 에디터 뷰 초기화
@@ -132,11 +163,9 @@ export class IdeDiff extends HTMLElement {
132
163
  doc: '',
133
164
  extensions: [
134
165
  basicExtensions,
135
- javascript(),
166
+ this.#languageCompartment.of(javascript()), // 초기 언어 설정
136
167
  EditorState.readOnly.of(true),
137
- this.#languageCompartment.of(javascript()),
138
- asisDiffDecorations,
139
- // ⭐️ updateListener는 유지: 뷰가 DOM에 완전히 렌더링될 때까지 기다림
168
+ asisDiffDecorations, // ASIS Diff 데코레이션 필드
140
169
  EditorView.updateListener.of((update) => {
141
170
  if (update.view.contentDOM.firstChild && !update.view._initialAsisContentLoaded) {
142
171
  update.view._initialAsisContentLoaded = true;
@@ -154,11 +183,9 @@ export class IdeDiff extends HTMLElement {
154
183
  doc: '',
155
184
  extensions: [
156
185
  basicExtensions,
157
- javascript(),
186
+ this.#languageCompartment.of(javascript()), // 초기 언어 설정
158
187
  EditorState.readOnly.of(true),
159
- this.#languageCompartment.of(javascript()),
160
- tobeDiffDecorations,
161
- // ⭐️ updateListener는 유지: 뷰가 DOM에 완전히 렌더링될 때까지 기다림
188
+ tobeDiffDecorations, // TOBE Diff 데코레이션 필드
162
189
  EditorView.updateListener.of((update) => {
163
190
  if (update.view.contentDOM.firstChild && !update.view._initialTobeContentLoaded) {
164
191
  update.view._initialTobeContentLoaded = true;
@@ -208,71 +235,49 @@ export class IdeDiff extends HTMLElement {
208
235
  this.#tobeEditorView.scrollDOM.addEventListener('scroll', this._tobeScrollHandler);
209
236
  };
210
237
 
211
- // IdeDiff 클래스 내부
238
+ // Diff 데코레이션 계산 및 반환 함수
212
239
  #applyDiffDecorations = (asisSrc, tobeSrc) => {
213
240
  const dmp = new diff_match_patch();
214
241
  const diffs = dmp.diff_main(asisSrc, tobeSrc);
215
242
  dmp.diff_cleanupSemantic(diffs);
216
243
 
217
- const asisLineDecos = [];
218
- const tobeLineDecos = [];
244
+ const asisLineBuilder = new RangeSetBuilder();
245
+ const tobeLineBuilder = new RangeSetBuilder();
219
246
 
220
- let asisCursor = 0;
221
- let tobeCursor = 0;
247
+ let asisCursor = 0; // ASIS 텍스트에서의 현재 커서 위치 (문자 오프셋)
248
+ let tobeCursor = 0; // TOBE 텍스트에서의 현재 커서 위치 (문자 오프셋)
222
249
 
223
- // 중요: 새로운 문서 내용을 기반으로 임시 Doc 객체를 생성합니다.
224
- // Doc 객체를 사용하여 lineAt()을 호출합니다.
225
- const asisDoc = Text.of(asisSrc.split('\n'));
226
- const tobeDoc = Text.of(tobeSrc.split('\n'));
250
+ // Decoration 인스턴스 미리 생성
251
+ const insertedLineDeco = Decoration.line({ class: "cm-inserted-line-bg" });
252
+ const deletedLineDeco = Decoration.line({ class: "cm-deleted-line-bg" });
227
253
 
228
254
  for (const [op, text] of diffs) {
229
- const len = text.length;
255
+ const len = text.length; // 현재 Diff 청크의 텍스트 길이
256
+ const linesInChunk = text.split('\n');
230
257
 
231
258
  switch (op) {
232
- case diff_match_patch.DIFF_INSERT: // Added text
233
- let currentTobeLineOffset = tobeCursor;
234
- const tobeLines = text.split('\n');
235
- for (let i = 0; i < tobeLines.length; i++) {
236
- // 줄의 시작 오프셋이 tobeDoc의 길이를 넘지 않는지 확인
237
- if (currentTobeLineOffset < tobeDoc.length) {
238
- const line = tobeDoc.lineAt(currentTobeLineOffset);
239
- tobeLineDecos.push({
240
- from: line.from,
241
- to: line.to,
242
- deco: Decoration.line({ class: "cm-inserted-line-bg" })
243
- });
244
- }
245
- // 다음 줄의 시작 오프셋 계산 (개행 문자 고려)
246
- currentTobeLineOffset += tobeLines[i].length + (i < tobeLines.length - 1 ? 1 : 0);
247
- // 마지막 줄이 비어 있고 개행으로 끝나지 않으면 루프 종료 (lineAt 오류 방지)
248
- if (i === tobeLines.length - 1 && tobeLines[i].length === 0 && text.endsWith('\n') === false) {
259
+ case diff_match_patch.DIFF_INSERT: // Added text (TOBE에만 존재)
260
+ for (let i = 0; i < linesInChunk.length; i++) {
261
+ // 마지막 줄이 비어있고 해당 청크가 개행으로 끝나지 않으면 스킵
262
+ if (i === linesInChunk.length - 1 && linesInChunk[i] === '' && !text.endsWith('\n')) {
263
+ tobeCursor += len; // tobeCursor만 해당 청크 길이만큼 증가
249
264
  break;
250
265
  }
266
+ // Decoration.line은 줄의 시작 위치에만 추가하면 CodeMirror가 해당 줄 전체에 적용합니다.
267
+ tobeLineBuilder.add(tobeCursor, tobeCursor, insertedLineDeco);
268
+ tobeCursor += linesInChunk[i].length + (i < linesInChunk.length - 1 ? 1 : 0);
251
269
  }
252
- tobeCursor += len;
253
270
  break;
254
271
 
255
- case diff_match_patch.DIFF_DELETE: // Deleted text
256
- let currentAsisLineOffset = asisCursor;
257
- const asisLines = text.split('\n');
258
- for (let i = 0; i < asisLines.length; i++) {
259
- // 줄의 시작 오프셋이 asisDoc의 길이를 넘지 않는지 확인
260
- if (currentAsisLineOffset < asisDoc.length) {
261
- const line = asisDoc.lineAt(currentAsisLineOffset);
262
- asisLineDecos.push({
263
- from: line.from,
264
- to: line.to,
265
- deco: Decoration.line({ class: "cm-deleted-line-bg" })
266
- });
267
- }
268
- // 다음 줄의 시작 오프셋 계산 (개행 문자 고려)
269
- currentAsisLineOffset += asisLines[i].length + (i < asisLines.length - 1 ? 1 : 0);
270
- // 마지막 줄이 비어 있고 개행으로 끝나지 않으면 루프 종료 (lineAt 오류 방지)
271
- if (i === asisLines.length - 1 && asisLines[i].length === 0 && text.endsWith('\n') === false) {
272
+ case diff_match_patch.DIFF_DELETE: // Deleted text (ASIS에만 존재)
273
+ for (let i = 0; i < linesInChunk.length; i++) {
274
+ if (i === linesInChunk.length - 1 && linesInChunk[i] === '' && !text.endsWith('\n')) {
275
+ asisCursor += len;
272
276
  break;
273
277
  }
278
+ asisLineBuilder.add(asisCursor, asisCursor, deletedLineDeco);
279
+ asisCursor += linesInChunk[i].length + (i < linesInChunk.length - 1 ? 1 : 0);
274
280
  }
275
- asisCursor += len;
276
281
  break;
277
282
 
278
283
  case diff_match_patch.DIFF_EQUAL: // Unchanged text
@@ -282,26 +287,9 @@ export class IdeDiff extends HTMLElement {
282
287
  }
283
288
  }
284
289
 
285
- // 데코레이션 중복 및 겹침 방지를 위해 정렬
286
- asisLineDecos.sort((a, b) => a.from - b.from);
287
- tobeLineDecos.sort((a, b) => a.from - b.from);
288
-
289
- // 라인 데코레이션 빌더 생성
290
- const asisLineBuilder = new RangeSetBuilder();
291
- for (const { from, to, deco } of asisLineDecos) {
292
- asisLineBuilder.add(from, to, deco);
293
- }
294
- const finalAsisDecorations = asisLineBuilder.finish(); // ⭐️ .update({ add: ... }) 부분 제거
295
-
296
- const tobeLineBuilder = new RangeSetBuilder();
297
- for (const { from, to, deco } of tobeLineDecos) {
298
- tobeLineBuilder.add(from, to, deco);
299
- }
300
- const finalTobeDecorations = tobeLineBuilder.finish(); // ⭐️ .update({ add: ... }) 부분 제거
301
-
302
290
  return {
303
- asisEffect: setAsisDecorationsEffect.of(finalAsisDecorations),
304
- tobeEffect: setTobeDecorationsEffect.of(finalTobeDecorations)
291
+ asisDecorations: asisLineBuilder.finish(),
292
+ tobeDecorations: tobeLineBuilder.finish()
305
293
  };
306
294
  };
307
295
 
@@ -318,16 +306,17 @@ export class IdeDiff extends HTMLElement {
318
306
  case 'javascript':
319
307
  langExtension = javascript();
320
308
  break;
309
+ // case 'java':
310
+ // langExtension = java();
311
+ // break;
312
+ // case 'xml':
313
+ // langExtension = xml();
314
+ // break;
321
315
  default:
322
- langExtension = javascript();
316
+ langExtension = javascript(); // 기본값
323
317
  }
324
318
 
325
- // 데코레이션 효과는 미리 계산해 둡니다.
326
- const { asisEffect, tobeEffect } = this.#applyDiffDecorations(src1, src2);
327
-
328
319
  // 1단계: 텍스트 내용 변경과 언어 확장을 먼저 디스패치합니다.
329
- // 'to' 값을 현재 문서 길이 전체로 지정하여 정확한 교체를 보장합니다.
330
-
331
320
  this.#asisEditorView.dispatch({
332
321
  changes: { from: 0, to: this.#asisEditorView.state.doc.length, insert: src1 },
333
322
  effects: [
@@ -342,27 +331,19 @@ export class IdeDiff extends HTMLElement {
342
331
  ]
343
332
  });
344
333
 
345
- console.log(asisEffect, tobeEffect);
346
-
347
- // ⭐️ 2단계: 텍스트 및 언어 변경이 완전히 적용되고 뷰가 안정화될 시간을 줍니다.
334
+ // 2단계: 텍스트 및 언어 변경이 완전히 적용되고 뷰가 안정화될 시간을 줍니다.
348
335
  // 다음 프레임에서 데코레이션 효과만 별도로 디스패치합니다.
349
336
  requestAnimationFrame(() => {
337
+ const { asisDecorations, tobeDecorations } = this.#applyDiffDecorations(src1, src2);
338
+
350
339
  this.#asisEditorView.dispatch({
351
- effects: [
352
- StateEffect.reconfigure.of(
353
- EditorView.decorations.of(
354
- Decoration.set(
355
- Decoration.line({ attributes: { style: "font-weight: bold" } }).range(0)
356
- )
357
- )
358
- )
359
- ]
340
+ effects: [setAsisDecorationsEffect.of(asisDecorations)] // StateField가 처리할 효과
360
341
  });
361
342
  this.#tobeEditorView.dispatch({
362
- //effects: [tobeEffect]
343
+ effects: [setTobeDecorationsEffect.of(tobeDecorations)] // StateField가 처리할 효과
363
344
  });
364
345
 
365
- // ⭐️ 3단계: 데코레이션까지 적용된 후 뷰가 다시 안정화될 시간을 한 번 더 줍니다.
346
+ // 3단계: 데코레이션까지 적용된 후 뷰가 다시 안정화될 시간을 한 번 더 줍니다.
366
347
  // 그 다음 프레임에서 스크롤 동기화를 활성화합니다.
367
348
  requestAnimationFrame(() => {
368
349
  this.#isScrollSyncActive = true;
@@ -372,21 +353,6 @@ export class IdeDiff extends HTMLElement {
372
353
  });
373
354
  });
374
355
  };
375
-
376
- disconnectedCallback() {
377
- // 컴포넌트 해제 시 스크롤 리스너 제거
378
- if (this._asisScrollHandler) {
379
- this.#asisEditorView.scrollDOM.removeEventListener('scroll', this._asisScrollHandler);
380
- this.#tobeEditorView.scrollDOM.removeEventListener('scroll', this._tobeScrollHandler);
381
- this._asisScrollHandler = null;
382
- this._tobeScrollHandler = null;
383
- }
384
- if (this.#asisEditorView) {
385
- this.#asisEditorView.destroy();
386
- }
387
- if (this.#tobeEditorView) {
388
- this.#tobeEditorView.destroy();
389
- }
390
- }
391
356
  }
357
+
392
358
  customElements.define("ide-diff", IdeDiff);