ide-assi 0.333.0 → 0.335.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,31 +1,72 @@
1
1
  import ninegrid from "ninegrid2";
2
2
 
3
3
  // CodeMirror 6 핵심 및 확장 임포트
4
- import { EditorView, lineNumbers, highlightSpecialChars, drawSelection, dropCursor, keymap, highlightActiveLine, highlightActiveLineGutter } from "@codemirror/view";
5
- import { EditorState, Compartment } from "@codemirror/state";
4
+ import {
5
+ EditorView, lineNumbers, highlightSpecialChars, drawSelection,
6
+ dropCursor, keymap, highlightActiveLine, highlightActiveLineGutter,
7
+ // 데코레이션을 위한 Decoration 클래스 임포트
8
+ Decoration
9
+ } from "@codemirror/view";
10
+ import {
11
+ EditorState, Compartment, StateField,
12
+ // 데코레이션 빌더를 위한 RangeSetBuilder 임포트
13
+ RangeSetBuilder
14
+ } from "@codemirror/state";
6
15
  import { history, historyKeymap, indentWithTab } from "@codemirror/commands";
7
- // ⭐️ 여기서 searchKeymap을 제거합니다.
8
- import { defaultKeymap, selectAll } from "@codemirror/commands"; // selectAll은 commands에 남아있습니다.
9
- // ⭐️ searchKeymap은 이제 @codemirror/search 에서 가져옵니다.
16
+ import { defaultKeymap, selectAll } from "@codemirror/commands";
10
17
  import { searchKeymap, highlightSelectionMatches } from "@codemirror/search";
11
18
  import { bracketMatching } from "@codemirror/language";
12
- import { javascript } from "@codemirror/lang-javascript"; // 예시 언어 (필요에 따라 다른 언어 팩 추가)
13
- import { indentOnInput, syntaxHighlighting, defaultHighlightStyle } from "@codemirror/language"; // syntaxHighlighting 추가
19
+ import { javascript } from "@codemirror/lang-javascript";
20
+ import { indentOnInput, syntaxHighlighting, defaultHighlightStyle } from "@codemirror/language";
14
21
  import { lintKeymap } from "@codemirror/lint";
15
22
  import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
16
23
 
17
24
  // Diff 로직을 위해 diff-match-patch 사용
18
25
  import { diff_match_patch } from 'diff-match-patch';
19
26
 
20
- // ... (나머지 IdeDiff 클래스 코드는 동일) ...
27
+ // Diff 데코레이션을 위한 StateField 정의 (asis 에디터용)
28
+ const asisDiffDecorations = StateField.define({
29
+ create() { return Decoration.none; }, // 초기 상태
30
+ update(decorations, tr) {
31
+ // 트랜잭션에 포함된 Effects를 처리하여 데코레이션을 업데이트
32
+ // `#applyDiffDecorations` 함수에서 데코레이션 업데이트 요청을 보낼 때 사용
33
+ return tr.effects.reduce((currentDecos, effect) => {
34
+ if (effect.is(setAsisDecorationsEffect)) {
35
+ return effect.value;
36
+ }
37
+ return currentDecos;
38
+ }, decorations);
39
+ },
40
+ // 데코레이션을 뷰에 제공
41
+ provide: f => EditorView.decorations.from(f)
42
+ });
43
+
44
+ // Diff 데코레이션을 위한 StateField 정의 (tobe 에디터용)
45
+ const tobeDiffDecorations = StateField.define({
46
+ create() { return Decoration.none; },
47
+ update(decorations, tr) {
48
+ return tr.effects.reduce((currentDecos, effect) => {
49
+ if (effect.is(setTobeDecorationsEffect)) {
50
+ return effect.value;
51
+ }
52
+ return currentDecos;
53
+ }, decorations);
54
+ },
55
+ provide: f => EditorView.decorations.from(f)
56
+ });
57
+
58
+ // 데코레이션 업데이트를 위한 Effect (트랜잭션에 포함시켜 상태 변경 알림)
59
+ const setAsisDecorationsEffect = EditorState.define.effect();
60
+ const setTobeDecorationsEffect = EditorState.define.effect();
61
+
21
62
 
22
63
  export class IdeDiff extends HTMLElement {
23
- #asisEditorView; // asis 패널의 CodeMirror EditorView 인스턴스
24
- #tobeEditorView; // tobe 패널의 CodeMirror EditorView 인스턴스
25
- #asisEditorEl; // asis 에디터를 렌더링할 DOM 요소
26
- #tobeEditorEl; // tobe 에디터를 렌더링할 DOM 요소
64
+ #asisEditorView;
65
+ #tobeEditorView;
66
+ #asisEditorEl;
67
+ #tobeEditorEl;
27
68
 
28
- #languageCompartment = new Compartment(); // 언어 확장을 동적으로 변경하기 위한 Compartment
69
+ #languageCompartment = new Compartment();
29
70
 
30
71
  constructor() {
31
72
  super();
@@ -49,27 +90,29 @@ export class IdeDiff extends HTMLElement {
49
90
  .panel {
50
91
  flex: 1; /* 패널들이 남은 공간을 채우도록 */
51
92
  overflow: hidden; /* CodeMirror EditorView 자체가 스크롤을 처리 */
52
- /* position: relative; */ /* CodeMirror가 자체적으로 관리하므로 필요 없을 수 있음 */
53
93
  min-width: 0; /* Flexbox 아이템이 content 때문에 늘어나는 것을 방지 */
54
94
  }
55
95
  .cm-editor {
56
96
  height: 100%; /* EditorView가 부모 div의 높이를 채우도록 */
57
97
  }
58
98
 
59
- /* Diff 시각화를 위한 기본 스타일 (나중에 데코레이션으로 더 정교하게) */
60
- .cm-line.cm-deleted-line {
61
- background-color: #ffeef0; /* 삭제된 배경 */
62
- }
63
- .cm-line.cm-inserted-line {
64
- background-color: #e6ffed; /* 삽입된 배경 */
99
+ /* Diff 시각화를 위한 CSS 클래스 */
100
+ /* 줄 전체 배경색 */
101
+ .cm-inserted-line-bg { background-color: #e6ffe6; } /* 연한 녹색 */
102
+ .cm-deleted-line-bg { background-color: #ffe6e6; } /* 연한 빨간색 */
103
+
104
+ /* 인라인(단어/문자) 변경 강조 */
105
+ .cm-inserted-inline {
106
+ background-color: #90ee90; /* 밝은 녹색 */
107
+ font-weight: bold;
65
108
  }
66
109
  .cm-deleted-inline {
67
- background-color: #ffcccc; /* 인라인 삭제 */
110
+ background-color: #ff9999; /* 밝은 빨간색 */
68
111
  text-decoration: line-through;
112
+ font-weight: bold;
69
113
  }
70
- .cm-inserted-inline {
71
- background-color: #ccffcc; /* 인라인 삽입 */
72
- }
114
+ /* 선택적: 동일하지만 이동된 줄 강조 (복잡함) */
115
+ /* .cm-moved-line-bg {} */
73
116
  </style>
74
117
 
75
118
  <div class="wrapper">
@@ -95,58 +138,59 @@ export class IdeDiff extends HTMLElement {
95
138
 
96
139
  // CodeMirror 확장 기본 설정
97
140
  const basicExtensions = [
98
- lineNumbers(), // 줄 번호
99
- highlightSpecialChars(), // 특수 문자 강조 (예: 공백)
100
- history(), // 실행 취소/다시 실행
101
- drawSelection(), // 선택 영역 표시
102
- dropCursor(), // 드래그 시 커서 위치
103
- EditorState.allowMultipleSelections.of(true), // 다중 선택 허용
104
- indentOnInput(), // 입력 시 자동 들여쓰기
105
- bracketMatching(), // 괄호 짝 맞추기
106
- highlightActiveLine(), // 현재 줄 강조
107
- highlightSelectionMatches(), // 선택된 텍스트와 일치하는 부분 강조
141
+ lineNumbers(),
142
+ highlightSpecialChars(),
143
+ history(),
144
+ drawSelection(),
145
+ dropCursor(),
146
+ EditorState.allowMultipleSelections.of(true),
147
+ indentOnInput(),
148
+ bracketMatching(),
149
+ highlightActiveLine(),
150
+ highlightSelectionMatches(),
108
151
  keymap.of([
109
- ...defaultKeymap, // 기본 키맵 (Enter, Backspace 등)
110
- ...searchKeymap, // 검색 관련 키맵
111
- ...historyKeymap, // undo/redo 키맵
112
- ...lintKeymap, // 린트 관련 키맵
113
- ...completionKeymap, // 자동 완성 키맵
114
- indentWithTab // 탭으로 들여쓰기 (필요시)
152
+ ...defaultKeymap,
153
+ ...searchKeymap,
154
+ ...historyKeymap,
155
+ ...lintKeymap,
156
+ ...completionKeymap,
157
+ indentWithTab,
158
+ selectAll // selectAll 추가
115
159
  ]),
116
- syntaxHighlighting(defaultHighlightStyle, { fallback: true }), // 기본 구문 하이라이팅
117
- autocompletion(), // 자동 완성
118
- // EditorState.readOnly.of(true) // 기본적으로 읽기 전용으로 설정
160
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
161
+ autocompletion(),
119
162
  ];
120
163
 
121
164
  // asis 에디터 생성 (읽기 전용)
122
165
  this.#asisEditorView = new EditorView({
123
166
  state: EditorState.create({
124
- doc: '', // 초기 내용 비워둠
167
+ doc: '',
125
168
  extensions: [
126
169
  basicExtensions,
127
- javascript(), // JS 언어 모드 (필요에 따라 변경)
170
+ javascript(), // JS 언어 모드 (기본)
128
171
  EditorState.readOnly.of(true), // asis는 읽기 전용
129
- this.#languageCompartment.of(javascript()) // 언어 변경을 위한 compartment
172
+ this.#languageCompartment.of(javascript()), // 언어 변경 compartment
173
+ asisDiffDecorations // asis 에디터에 Diff 데코레이션 필드 추가
130
174
  ]
131
175
  }),
132
176
  parent: this.#asisEditorEl
133
177
  });
134
178
 
135
- // tobe 에디터 생성 (읽기 전용, 또는 편집 가능하게 할 수도 있음)
179
+ // tobe 에디터 생성 (읽기 전용, 필요 편집 가능하게 설정)
136
180
  this.#tobeEditorView = new EditorView({
137
181
  state: EditorState.create({
138
- doc: '', // 초기 내용 비워둠
182
+ doc: '',
139
183
  extensions: [
140
184
  basicExtensions,
141
- javascript(), // JS 언어 모드 (필요에 따라 변경)
142
- EditorState.readOnly.of(true), // tobe도 읽기 전용으로 설정 (필요시 false)
143
- this.#languageCompartment.of(javascript()) // 언어 변경을 위한 compartment
185
+ javascript(), // JS 언어 모드 (기본)
186
+ EditorState.readOnly.of(true), // tobe도 읽기 전용
187
+ this.#languageCompartment.of(javascript()), // 언어 변경 compartment
188
+ tobeDiffDecorations // tobe 에디터에 Diff 데코레이션 필드 추가
144
189
  ]
145
190
  }),
146
191
  parent: this.#tobeEditorEl
147
192
  });
148
193
 
149
- // 스크롤 동기화 설정
150
194
  this.#setupScrollSync();
151
195
  };
152
196
 
@@ -173,44 +217,101 @@ export class IdeDiff extends HTMLElement {
173
217
  });
174
218
  };
175
219
 
176
- // Diff 계산하고 에디터에 적용하는 메서드
177
- #renderDiff = (asisSrc, tobeSrc) => {
220
+ // ⭐️⭐️⭐️ Diff 데코레이션 적용 로직 구현 ⭐️⭐️⭐️
221
+ #applyDiffDecorations = (asisSrc, tobeSrc) => {
178
222
  const dmp = new diff_match_patch();
179
223
  const diffs = dmp.diff_main(asisSrc, tobeSrc);
180
224
  dmp.diff_cleanupSemantic(diffs);
181
225
 
182
- // TODO: diffs 결과를 바탕으로 CodeMirror 데코레이션(Decoration)을 생성하고 에디터에 적용하는 로직 구현
183
- // 부분은 CodeMirror의 Decoration, StateField, ViewPlugin 등을 이해하고 구현해야 합니다.
184
- // 각 줄에 대한 변경 상태(삽입/삭제/동일)를 판단하고, 인라인 변경(단어 단위)도 처리해야 합니다.
185
- // 예를 들어:
186
- // const asisDecorations = [];
187
- // const tobeDecorations = [];
188
- // let asisCursor = 0;
189
- // let tobeCursor = 0;
190
- // for (const [op, text] of diffs) {
191
- // const len = text.length;
192
- // if (op === diff_match_patch.DIFF_INSERT) {
193
- // // tobe 쪽에 삽입된 부분 데코레이션 추가
194
- // tobeDecorations.push(Decoration.mark({class: "cm-inserted-inline"}).range(tobeCursor, tobeCursor + len));
195
- // tobeCursor += len;
196
- // } else if (op === diff_match_patch.DIFF_DELETE) {
197
- // // asis 쪽에 삭제된 부분 데코레이션 추가
198
- // asisDecorations.push(Decoration.mark({class: "cm-deleted-inline"}).range(asisCursor, asisCursor + len));
199
- // asisCursor += len;
200
- // } else { // DIFF_EQUAL
201
- // asisCursor += len;
202
- // tobeCursor += len;
203
- // }
204
- // }
205
- // 에디터 상태를 업데이트하고 데코레이션을 적용하는 복잡한 로직이 필요합니다.
206
- // 이 예시에서는 생략하지만, 실제 구현 시 핵심이 됩니다.
207
-
208
- // 현재는 단순히 텍스트만 업데이트
226
+ const asisBuilder = new RangeSetBuilder();
227
+ const tobeBuilder = new RangeSetBuilder();
228
+
229
+ let asisCursor = 0; // asis 에디터에서의 현재 텍스트 오프셋
230
+ let tobeCursor = 0; // tobe 에디터에서의 현재 텍스트 오프셋
231
+
232
+ // 단위 데코레이션을 위해 줄 시작 위치를 추적
233
+ const asisLineStartMap = new Map();
234
+ const tobeLineStartMap = new Map();
235
+
236
+ // 텍스트에 대한 줄 시작 위치 미리 계산
237
+ let currentPos = 0;
238
+ for (const line of asisSrc.split('\n')) {
239
+ asisLineStartMap.set(currentPos, true);
240
+ currentPos += line.length + 1; // +1은 줄바꿈 문자 포함
241
+ }
242
+ currentPos = 0;
243
+ for (const line of tobeSrc.split('\n')) {
244
+ tobeLineStartMap.set(currentPos, true);
245
+ currentPos += line.length + 1;
246
+ }
247
+
248
+ for (const [op, text] of diffs) {
249
+ const len = text.length;
250
+
251
+ switch (op) {
252
+ case diff_match_patch.DIFF_INSERT: // Added text
253
+ // tobe 쪽에 인라인 데코레이션 추가
254
+ tobeBuilder.add(
255
+ tobeCursor, tobeCursor + len,
256
+ Decoration.mark({ class: "cm-inserted-inline" })
257
+ );
258
+
259
+ // tobe 쪽에 줄 전체 데코레이션 추가 (삽입된 줄)
260
+ // text가 여러 줄일 수 있으므로 각 줄에 적용
261
+ const tobeLines = text.split('\n');
262
+ let currentLineOffset = tobeCursor;
263
+ for(let i = 0; i < tobeLines.length; i++) {
264
+ // 각 줄의 시작 위치를 찾기
265
+ const lineStart = tobeEditorView.state.doc.lineAt(currentLineOffset).from;
266
+ // 만약 다음 줄이 있다면 해당 줄의 끝까지, 마지막 줄이라면 텍스트의 끝까지
267
+ const lineEnd = i === tobeLines.length - 1 ? currentLineOffset + tobeLines[i].length : tobeEditorView.state.doc.lineAt(currentLineOffset).to;
268
+ tobeBuilder.add(
269
+ lineStart, lineEnd, // 줄의 실제 시작부터 끝까지
270
+ Decoration.line({ class: "cm-inserted-line-bg" })
271
+ );
272
+ currentLineOffset += tobeLines[i].length + 1; // 다음 줄 시작 위치로 이동
273
+ }
274
+
275
+
276
+ tobeCursor += len;
277
+ break;
278
+
279
+ case diff_match_patch.DIFF_DELETE: // Deleted text
280
+ // asis 쪽에 인라인 데코레이션 추가
281
+ asisBuilder.add(
282
+ asisCursor, asisCursor + len,
283
+ Decoration.mark({ class: "cm-deleted-inline" })
284
+ );
285
+
286
+ // asis 쪽에 줄 전체 데코레이션 추가 (삭제된 줄)
287
+ const asisLines = text.split('\n');
288
+ let currentAsisLineOffset = asisCursor;
289
+ for(let i = 0; i < asisLines.length; i++) {
290
+ const lineStart = asisEditorView.state.doc.lineAt(currentAsisLineOffset).from;
291
+ const lineEnd = i === asisLines.length - 1 ? currentAsisLineOffset + asisLines[i].length : asisEditorView.state.doc.lineAt(currentAsisLineOffset).to;
292
+ asisBuilder.add(
293
+ lineStart, lineEnd,
294
+ Decoration.line({ class: "cm-deleted-line-bg" })
295
+ );
296
+ currentAsisLineOffset += asisLines[i].length + 1;
297
+ }
298
+
299
+ asisCursor += len;
300
+ break;
301
+
302
+ case diff_match_patch.DIFF_EQUAL: // Unchanged text
303
+ asisCursor += len;
304
+ tobeCursor += len;
305
+ break;
306
+ }
307
+ }
308
+
309
+ // 에디터 뷰에 데코레이션 업데이트 요청 디스패치
209
310
  this.#asisEditorView.dispatch({
210
- changes: { from: 0, to: this.#asisEditorView.state.doc.length, insert: asisSrc }
311
+ effects: setAsisDecorationsEffect.of(asisBuilder.finish())
211
312
  });
212
313
  this.#tobeEditorView.dispatch({
213
- changes: { from: 0, to: this.#tobeEditorView.state.doc.length, insert: tobeSrc }
314
+ effects: setTobeDecorationsEffect.of(tobeBuilder.finish())
214
315
  });
215
316
  };
216
317
 
@@ -227,13 +328,15 @@ export class IdeDiff extends HTMLElement {
227
328
  langExtension = javascript();
228
329
  break;
229
330
  // case 'html':
230
- // langExtension = html(); // @codemirror/lang-html 임포트 필요
331
+ // import { html } from "@codemirror/lang-html"; // 상단에 임포트 필요
332
+ // langExtension = html();
231
333
  // break;
232
334
  // case 'css':
233
- // langExtension = css(); // @codemirror/lang-css 임포트 필요
335
+ // import { css } from "@codemirror/lang-css"; // 상단에 임포트 필요
336
+ // langExtension = css();
234
337
  // break;
235
338
  default:
236
- langExtension = javascript(); // 기본값
339
+ langExtension = javascript();
237
340
  }
238
341
 
239
342
  // 언어 확장 업데이트
@@ -244,11 +347,19 @@ export class IdeDiff extends HTMLElement {
244
347
  effects: this.#languageCompartment.reconfigure(langExtension)
245
348
  });
246
349
 
247
- this.#renderDiff(src1, src2);
350
+ // 텍스트 내용 업데이트
351
+ this.#asisEditorView.dispatch({
352
+ changes: { from: 0, to: this.#asisEditorView.state.doc.length, insert: src1 }
353
+ });
354
+ this.#tobeEditorView.dispatch({
355
+ changes: { from: 0, to: this.#tobeEditorView.state.doc.length, insert: src2 }
356
+ });
357
+
358
+ // Diff 데코레이션 적용
359
+ this.#applyDiffDecorations(src1, src2);
248
360
  };
249
361
 
250
362
  disconnectedCallback() {
251
- // 컴포넌트가 DOM에서 제거될 때 에디터 인스턴스 정리
252
363
  if (this.#asisEditorView) {
253
364
  this.#asisEditorView.destroy();
254
365
  }