Skip to content

Commit faf747c

Browse files
committedJan 18, 2025
[WIP] Add lineComments CodeMirror extension
1 parent 2420ab3 commit faf747c

File tree

8 files changed

+262
-4
lines changed

8 files changed

+262
-4
lines changed
 

‎app/components/code-mirror.hbs

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
{{did-update this.optionDidChange "indentWithTab" @indentWithTab}}
2323
{{did-update this.optionDidChange "languageOrFilename" @filename}}
2424
{{did-update this.optionDidChange "languageOrFilename" @language}}
25+
{{did-update this.optionDidChange "lineComments" @lineComments}}
2526
{{did-update this.optionDidChange "lineNumbers" @lineNumbers}}
2627
{{did-update this.optionDidChange "lineSeparator" @lineSeparator}}
2728
{{did-update this.optionDidChange "lineWrapping" @lineWrapping}}

‎app/components/code-mirror.ts

+6
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { languages } from '@codemirror/language-data';
3838
import { markdown } from '@codemirror/lang-markdown';
3939
import { highlightNewlines } from 'codecrafters-frontend/utils/code-mirror-highlight-newlines';
4040
import { collapseUnchangedGutter } from 'codecrafters-frontend/utils/code-mirror-collapse-unchanged-gutter';
41+
import { lineComments } from 'codecrafters-frontend/utils/code-mirror-line-comments';
4142

4243
function generateHTMLElement(src: string): HTMLElement {
4344
const div = document.createElement('div');
@@ -76,6 +77,7 @@ const OPTION_HANDLERS: { [key: string]: OptionHandler } = {
7677
indentOnInput: ({ indentOnInput: enabled }) => (enabled ? [indentOnInput()] : []),
7778
indentUnit: ({ indentUnit: indentUnitText }) => (indentUnitText !== undefined ? [indentUnit.of(indentUnitText)] : []),
7879
indentWithTab: ({ indentWithTab: enabled }) => (enabled ? [keymap.of([indentWithTab])] : []),
80+
lineComments: ({ lineComments: enabled }) => (enabled ? [lineComments()] : []),
7981
lineNumbers: ({ lineNumbers: enabled }) => (enabled ? [lineNumbers()] : []),
8082
foldGutter: ({ foldGutter: enabled }) =>
8183
enabled
@@ -264,6 +266,10 @@ export interface Signature {
264266
* Enable indentation of lines or selection using TAB and Shift+TAB keys, otherwise editor loses focus when TAB is pressed
265267
*/
266268
indentWithTab?: boolean;
269+
/**
270+
* Enable line comments
271+
*/
272+
lineComments?: boolean;
267273
/**
268274
* Enable the line numbers gutter
269275
*/

‎app/controllers/demo/code-mirror.ts

+3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const OPTION_DEFAULTS = {
5454
indentWithTab: true,
5555
language: true,
5656
lineNumbers: true,
57+
lineComments: false,
5758
lineSeparator: true,
5859
lineWrapping: true,
5960
maxHeight: true,
@@ -108,6 +109,7 @@ export default class DemoCodeMirrorController extends Controller {
108109
'indentUnit',
109110
'indentWithTab',
110111
'language',
112+
'lineComments',
111113
'lineNumbers',
112114
'lineSeparator',
113115
'lineWrapping',
@@ -160,6 +162,7 @@ export default class DemoCodeMirrorController extends Controller {
160162
@tracked indentUnits = INDENT_UNITS;
161163
@tracked indentWithTab = OPTION_DEFAULTS.indentWithTab;
162164
@tracked language = OPTION_DEFAULTS.language;
165+
@tracked lineComments = OPTION_DEFAULTS.lineComments;
163166
@tracked lineNumbers = OPTION_DEFAULTS.lineNumbers;
164167
@tracked lineSeparator = OPTION_DEFAULTS.lineSeparator;
165168
@tracked lineSeparators = LINE_SEPARATORS;

‎app/routes/demo/code-mirror.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const QUERY_PARAMS = [
2525
'indentUnit',
2626
'indentWithTab',
2727
'language',
28+
'lineComments',
2829
'lineNumbers',
2930
'lineSeparator',
3031
'lineWrapping',

‎app/templates/demo/code-mirror.hbs

+7-2
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@
167167
<Input @type="checkbox" @checked={{this.foldGutter}} />
168168
<span class="ml-2">foldGutter</span>
169169
</label>
170+
<label class="{{labelClasses}}" title="Enable line comments">
171+
<Input @type="checkbox" @checked={{this.lineComments}} />
172+
<span class="ml-2">lineComments</span>
173+
</label>
174+
</codemirror-options-left>
175+
<codemirror-options-right class="flex flex-wrap">
170176
<label class="{{labelClasses}}" title="Enable visual line wrapping for lines exceeding editor width">
171177
<Input @type="checkbox" @checked={{this.lineWrapping}} />
172178
<span class="ml-2">lineWrapping</span>
@@ -175,8 +181,6 @@
175181
<Input @type="checkbox" @checked={{this.scrollPastEnd}} />
176182
<span class="ml-2">scrollPastEnd</span>
177183
</label>
178-
</codemirror-options-left>
179-
<codemirror-options-right class="flex flex-wrap">
180184
<label class="{{labelClasses}}" title="Limit maximum height of the component's element">
181185
<Input @type="checkbox" @checked={{this.maxHeight}} />
182186
<span class="ml-2">maxHeight</span>
@@ -340,6 +344,7 @@
340344
@history={{this.history}}
341345
@indentOnInput={{this.indentOnInput}}
342346
@indentWithTab={{this.indentWithTab}}
347+
@lineComments={{this.lineComments}}
343348
@lineNumbers={{this.lineNumbers}}
344349
@lineWrapping={{this.lineWrapping}}
345350
@mergeControls={{this.mergeControls}}
+226
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { BlockInfo, Decoration, EditorView, WidgetType, type DecorationSet } from '@codemirror/view';
2+
import { EditorState, Facet, Line, StateField } from '@codemirror/state';
3+
import {
4+
gutter as gutterRS,
5+
GutterMarker as GutterMarkerRS,
6+
highlightActiveLineGutter as highlightActiveLineGutterRS,
7+
} from 'codecrafters-frontend/utils/code-mirror-gutter-rs';
8+
9+
function getRandomInt(inclusiveMin: number, exclusiveMax: number) {
10+
const minCeiled = Math.ceil(inclusiveMin);
11+
const maxFloored = Math.floor(exclusiveMax);
12+
13+
return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled);
14+
}
15+
16+
function generateRandomComments(linesCount = 0) {
17+
return Array.from({ length: linesCount }).map(function (_v, lineNumber) {
18+
const rnd = Math.random();
19+
20+
let commentsCount;
21+
22+
if (rnd < 0.05) {
23+
commentsCount = getRandomInt(100, 1000);
24+
} else if (rnd < 0.1) {
25+
commentsCount = getRandomInt(10, 100);
26+
} else if (rnd < 0.8) {
27+
commentsCount = 0;
28+
} else {
29+
commentsCount = getRandomInt(1, 10);
30+
}
31+
32+
return Array.from<string>({ length: commentsCount }).map(
33+
(_v, index) => new LineComment({ lineNumber, author: 'Darth Programmius', text: `Comment #${index}` }),
34+
);
35+
});
36+
}
37+
38+
type LineCommentsCollection = (undefined | LineComment[])[];
39+
40+
class LineComment {
41+
lineNumber: number;
42+
text: string;
43+
author: string;
44+
45+
constructor({ lineNumber, text, author }: { lineNumber: number; text: string; author: string }) {
46+
this.lineNumber = lineNumber;
47+
this.text = text;
48+
this.author = author;
49+
}
50+
}
51+
52+
class CommentsWidget extends WidgetType {
53+
line: Line;
54+
55+
constructor(line: Line) {
56+
super();
57+
this.line = line;
58+
}
59+
60+
toDOM(view: EditorView): HTMLElement {
61+
const comments = (view.state.facet(lineCommentsFacet)[0] || [])[this.line.number - 1];
62+
const elem = document.createElement('line-comments');
63+
64+
if (comments?.length) {
65+
elem.innerText = `💬 COMMENTS (${comments?.length || 0}) FOR LINE #${this.line.number}`;
66+
}
67+
68+
return elem;
69+
}
70+
}
71+
72+
function lineCommentsDecorations(state: EditorState) {
73+
const decorations = [];
74+
75+
for (let i = 1; i <= state.doc.lines; i++) {
76+
const line = state.doc.line(i);
77+
decorations.push(
78+
Decoration.widget({
79+
widget: new CommentsWidget(line),
80+
side: 10,
81+
// inlineOrder: true,
82+
// block: true,
83+
}).range(line.to),
84+
);
85+
}
86+
87+
return Decoration.set(decorations);
88+
}
89+
90+
const lineCommentsStateField = StateField.define<DecorationSet>({
91+
create(state) {
92+
return lineCommentsDecorations(state);
93+
},
94+
update(_value, tr) {
95+
return lineCommentsDecorations(tr.state);
96+
// return tr.docChanged ? lineCommentsDecorations(tr.state) : value;
97+
},
98+
provide(field) {
99+
return EditorView.decorations.from(field);
100+
},
101+
});
102+
103+
const lineCommentsFacet = Facet.define<LineCommentsCollection>();
104+
105+
class CommentsCountGutterMarker extends GutterMarkerRS {
106+
line: BlockInfo;
107+
108+
constructor(line: BlockInfo) {
109+
super();
110+
this.line = line;
111+
}
112+
113+
toDOM(view: EditorView) {
114+
const lineNumber = view.state.doc.lineAt(this.line.from).number;
115+
const comments = (view.state.facet(lineCommentsFacet)[0] || [])[lineNumber - 1];
116+
const commentsCount = comments?.length || 0;
117+
const elem = document.createElement('comments-count');
118+
119+
elem.innerText = `${commentsCount > 99 ? '99+' : commentsCount}`;
120+
121+
if (commentsCount > 99) {
122+
elem.className = 'cm-over-99';
123+
}
124+
125+
return elem;
126+
}
127+
}
128+
129+
class CommentButtonGutterMarker extends GutterMarkerRS {
130+
line: BlockInfo;
131+
132+
constructor(line: BlockInfo) {
133+
super();
134+
this.line = line;
135+
}
136+
137+
toDOM() {
138+
const elem = document.createElement('comment-button');
139+
140+
elem.innerText = `💬`;
141+
142+
return elem;
143+
}
144+
}
145+
146+
export function lineComments() {
147+
return [
148+
lineCommentsFacet.compute(['doc'], (state) => generateRandomComments(state.doc.lines)),
149+
150+
lineCommentsStateField,
151+
152+
gutterRS({
153+
class: 'cm-lineCommentsGutter',
154+
155+
lineMarker(view, line) {
156+
const lineNumber = view.state.doc.lineAt(line.from).number;
157+
const comments = (view.state.facet(lineCommentsFacet)[0] || [])[lineNumber - 1];
158+
const commentsCount = comments?.length || 0;
159+
160+
return new (commentsCount === 0 ? CommentButtonGutterMarker : CommentsCountGutterMarker)(line);
161+
},
162+
}),
163+
164+
highlightActiveLineGutterRS(),
165+
166+
EditorView.baseTheme({
167+
'.cm-line': {
168+
'& line-comments': {
169+
display: 'block',
170+
backgroundColor: '#009bff40',
171+
paddingLeft: '1rem',
172+
marginRight: '-1rem',
173+
174+
'& + br': {
175+
display: 'none',
176+
},
177+
},
178+
179+
'& .cm-insertedLine + br': {
180+
display: 'none',
181+
},
182+
},
183+
184+
'.cm-gutters.cm-gutters-rs': {
185+
backgroundColor: '#ffffff20', // '#ff000070', // 'transparent',
186+
187+
'& .cm-lineCommentsGutter': {
188+
minWidth: '24px',
189+
textAlign: 'center',
190+
191+
'& .cm-gutterElement': {
192+
cursor: 'pointer',
193+
194+
'& comments-count': {
195+
display: 'block',
196+
backgroundColor: '#ffcd72c0',
197+
borderRadius: '50%',
198+
color: '#24292e',
199+
transform: 'scale(0.75)',
200+
fontWeight: '500',
201+
fontSize: '12px',
202+
203+
'&.cm-over-99': {
204+
fontSize: '9.5px',
205+
},
206+
},
207+
208+
'& comment-button': {
209+
opacity: '0.15',
210+
},
211+
212+
'&:hover': {
213+
'& comment-button': {
214+
opacity: '1',
215+
},
216+
217+
'& comments-count': {
218+
backgroundColor: '#ffa500',
219+
},
220+
},
221+
},
222+
},
223+
},
224+
}),
225+
];
226+
}

‎app/utils/code-mirror-themes.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const BASE_STYLE = {
1818
'.cm-lineNumbers': {
1919
'& .cm-gutterElement': {
2020
display: 'flex',
21-
alignItems: 'center',
21+
alignItems: 'flex-start',
2222
justifyContent: 'flex-end',
2323
padding: '0 0.5rem 0 1rem',
2424
fontSize: '0.875em',
@@ -30,11 +30,15 @@ const BASE_STYLE = {
3030
'.cm-foldGutter': {
3131
'& .cm-gutterElement': {
3232
display: 'flex',
33-
alignItems: 'center',
33+
alignItems: 'flex-start',
3434
justifyContent: 'center',
3535
fontSize: '0.875em',
3636
minWidth: '1.2rem',
3737
color: '#94a3b8',
38+
39+
'& svg': {
40+
marginTop: '0.3rem',
41+
},
3842
},
3943
},
4044

‎tests/integration/components/code-mirror-test.js

+12
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,18 @@ module('Integration | Component | code-mirror', function (hooks) {
350350
skip('it does something useful with the editor');
351351
});
352352

353+
module('lineComments', function () {
354+
test("it doesn't break the editor when passed", async function (assert) {
355+
this.set('lineComments', true);
356+
await render(hbs`<CodeMirror @lineComments={{this.lineComments}} />`);
357+
assert.ok(codeMirror.hasRendered);
358+
this.set('lineComments', false);
359+
assert.ok(codeMirror.hasRendered);
360+
});
361+
362+
skip('it does something useful with the editor');
363+
});
364+
353365
module('lineNumbers', function () {
354366
test("it doesn't break the editor when passed", async function (assert) {
355367
this.set('lineNumbers', true);

0 commit comments

Comments
 (0)
Failed to load comments.