Skip to content

Commit 15c0591

Browse files
committed
[WIP] Add lineComments CodeMirror extension
1 parent 8803899 commit 15c0591

7 files changed

Lines changed: 197 additions & 2 deletions

File tree

app/components/code-mirror.hbs

Lines changed: 1 addition & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 7 additions & 2 deletions
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}}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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+
type LineCommentsCollection = (undefined | LineComment[])[];
17+
18+
class LineComment {
19+
lineNumber: number;
20+
text: string;
21+
author: string;
22+
23+
constructor({ lineNumber, text, author }: { lineNumber: number; text: string; author: string }) {
24+
this.lineNumber = lineNumber;
25+
this.text = text;
26+
this.author = author;
27+
}
28+
}
29+
30+
class CommentsWidget extends WidgetType {
31+
line: Line;
32+
33+
constructor(line: Line) {
34+
super();
35+
this.line = line;
36+
}
37+
38+
toDOM(view: EditorView): HTMLElement {
39+
const comments = (view.state.facet(lineCommentsFacet)[0] || [])[this.line.number - 1];
40+
41+
const elem = document.createElement('line-comments');
42+
elem.style.display = 'block';
43+
elem.style.backgroundColor = '#009bff80';
44+
elem.style.paddingLeft = '1rem';
45+
46+
if (comments?.length) {
47+
elem.innerText = `💬 COMMENTS (${comments?.length || 0}) FOR LINE #${this.line.number}`;
48+
}
49+
50+
return elem;
51+
}
52+
}
53+
54+
function lineCommentsDecorations(state: EditorState) {
55+
const decorations = [];
56+
57+
for (let i = 1; i <= state.doc.lines; i++) {
58+
const line = state.doc.line(i);
59+
decorations.push(
60+
Decoration.widget({
61+
widget: new CommentsWidget(line),
62+
side: 10,
63+
inlineOrder: true,
64+
block: true,
65+
}).range(line.to),
66+
);
67+
}
68+
69+
return Decoration.set(decorations);
70+
}
71+
72+
const lineCommentsStateField = StateField.define<DecorationSet>({
73+
create(state) {
74+
return lineCommentsDecorations(state);
75+
},
76+
update(_value, tr) {
77+
return lineCommentsDecorations(tr.state);
78+
// return tr.docChanged ? lineCommentsDecorations(tr.state) : value;
79+
},
80+
provide(field) {
81+
return EditorView.decorations.from(field);
82+
},
83+
});
84+
85+
const lineCommentsFacet = Facet.define<LineCommentsCollection>();
86+
87+
class CommentsCountGutterMarker extends GutterMarkerRS {
88+
line: BlockInfo;
89+
90+
constructor(line: BlockInfo) {
91+
super();
92+
this.line = line;
93+
}
94+
95+
toDOM(view: EditorView) {
96+
const lineNumber = view.state.doc.lineAt(this.line.from).number;
97+
const comments = (view.state.facet(lineCommentsFacet)[0] || [])[lineNumber - 1];
98+
const elem = document.createElement('comments-count');
99+
elem.innerText = `${comments?.length}`;
100+
101+
return elem;
102+
}
103+
}
104+
105+
class CommentButtonGutterMarker extends GutterMarkerRS {
106+
line: BlockInfo;
107+
108+
constructor(line: BlockInfo) {
109+
super();
110+
this.line = line;
111+
}
112+
113+
toDOM() {
114+
const elem = document.createElement('comment-button');
115+
elem.innerText = `💬`;
116+
117+
return elem;
118+
}
119+
}
120+
121+
export function lineComments() {
122+
return [
123+
lineCommentsFacet.compute(['doc'], function (state) {
124+
return Array.from({ length: state.doc.lines }).map((_v, lineNumber) =>
125+
Array.from<string>({ length: Math.random() < 0.4 ? getRandomInt(0, 6) : 0 }).map(
126+
(_v, index) => new LineComment({ lineNumber, author: 'Darth Programmius', text: `Comment #${index}` }),
127+
),
128+
);
129+
}),
130+
lineCommentsStateField,
131+
gutterRS({
132+
class: 'cm-custom-gutter-rs',
133+
renderEmptyElements: true,
134+
lineMarker(view, line) {
135+
const lineNumber = view.state.doc.lineAt(line.from).number;
136+
const comments = (view.state.facet(lineCommentsFacet)[0] || [])[lineNumber - 1];
137+
138+
return comments?.length === 0 ? new CommentButtonGutterMarker(line) : new CommentsCountGutterMarker(line);
139+
},
140+
}),
141+
highlightActiveLineGutterRS(),
142+
EditorView.baseTheme({
143+
'.cm-gutters.cm-gutters-rs': {
144+
backgroundColor: '#ff000070', // 'transparent',
145+
146+
'& .cm-custom-gutter-rs': {
147+
minWidth: '24px',
148+
textAlign: 'center',
149+
150+
'& .cm-gutterElement': {
151+
cursor: 'pointer',
152+
153+
'& comment-button': {
154+
opacity: '0.2',
155+
},
156+
157+
'&:hover': {
158+
'& comment-button': {
159+
opacity: '1',
160+
},
161+
},
162+
},
163+
},
164+
},
165+
}),
166+
];
167+
}

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

Lines changed: 12 additions & 0 deletions
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)