Skip to content

Add a new lineComments extension for CodeMirror #2549

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/components/code-mirror.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
{{did-update this.optionDidChange "indentWithTab" @indentWithTab}}
{{did-update this.optionDidChange "languageOrFilename" @filename}}
{{did-update this.optionDidChange "languageOrFilename" @language}}
{{did-update this.optionDidChange "lineCommentsOrLineData" (array @lineComments @lineData)}}
{{did-update this.optionDidChange "lineNumbers" @lineNumbers}}
{{did-update this.optionDidChange "lineSeparator" @lineSeparator}}
{{did-update this.optionDidChange "lineWrapping" @lineWrapping}}
Expand Down
18 changes: 17 additions & 1 deletion app/components/code-mirror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { markdown } from '@codemirror/lang-markdown';
import { highlightNewlines } from 'codecrafters-frontend/utils/code-mirror-highlight-newlines';
import { collapseUnchangedGutter } from 'codecrafters-frontend/utils/code-mirror-collapse-unchanged-gutter';
import { highlightActiveLineGutter as highlightActiveLineGutterRS } from 'codecrafters-frontend/utils/code-mirror-gutter-rs';
import { lineComments, type LineDataCollection } from 'codecrafters-frontend/utils/code-mirror-line-comments';

function generateHTMLElement(src: string): HTMLElement {
const div = document.createElement('div');
Expand All @@ -54,7 +55,7 @@ enum FoldGutterIcon {

type DocumentUpdateCallback = (newValue: string) => void;

type Argument = boolean | string | number | undefined | Extension | DocumentUpdateCallback;
type Argument = boolean | string | number | undefined | Extension | DocumentUpdateCallback | LineDataCollection;

type OptionHandler = (args: Signature['Args']['Named']) => Extension[] | Promise<Extension[]>;

Expand All @@ -78,6 +79,7 @@ const OPTION_HANDLERS: { [key: string]: OptionHandler } = {
indentOnInput: ({ indentOnInput: enabled }) => (enabled ? [indentOnInput()] : []),
indentUnit: ({ indentUnit: indentUnitText }) => (indentUnitText !== undefined ? [indentUnit.of(indentUnitText)] : []),
indentWithTab: ({ indentWithTab: enabled }) => (enabled ? [keymap.of([indentWithTab])] : []),
lineCommentsOrLineData: ({ lineData, lineComments: enabled }) => (enabled && lineData ? [lineComments(lineData)] : []),
lineNumbers: ({ lineNumbers: enabled }) => (enabled ? [lineNumbers()] : []),
foldGutter: ({ foldGutter: enabled }) =>
enabled
Expand Down Expand Up @@ -272,6 +274,14 @@ export interface Signature {
* Enable indentation of lines or selection using TAB and Shift+TAB keys, otherwise editor loses focus when TAB is pressed
*/
indentWithTab?: boolean;
/**
* Enable line comments
*/
lineComments?: boolean;
/**
* Line data containing comments counts or other line-related metadata
*/
lineData?: LineDataCollection;
/**
* Enable the line numbers gutter
*/
Expand Down Expand Up @@ -371,6 +381,12 @@ export default class CodeMirrorComponent extends Component<Signature> {
});
}

if (optionName === 'lineCommentsOrLineData') {
this.#updateRenderedView({
effects: this.#resetCompartment('lineCommentsOrLineData'),
});
}

// Reconfigure the changed compartment with new options and dispatch new effects to the view
this.#updateRenderedView({
effects: await this.#updateCompartment(optionName),
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/demo/code-mirror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const OPTION_DEFAULTS = {
indentWithTab: true,
language: true,
lineNumbers: true,
lineComments: false,
lineSeparator: true,
lineWrapping: true,
maxHeight: true,
Expand Down Expand Up @@ -110,6 +111,7 @@ export default class DemoCodeMirrorController extends Controller {
'indentUnit',
'indentWithTab',
'language',
'lineComments',
'lineNumbers',
'lineSeparator',
'lineWrapping',
Expand Down Expand Up @@ -163,6 +165,7 @@ export default class DemoCodeMirrorController extends Controller {
@tracked indentUnits = INDENT_UNITS;
@tracked indentWithTab = OPTION_DEFAULTS.indentWithTab;
@tracked language = OPTION_DEFAULTS.language;
@tracked lineComments = OPTION_DEFAULTS.lineComments;
@tracked lineNumbers = OPTION_DEFAULTS.lineNumbers;
@tracked lineSeparator = OPTION_DEFAULTS.lineSeparator;
@tracked lineSeparators = LINE_SEPARATORS;
Expand Down
1 change: 1 addition & 0 deletions app/routes/demo/code-mirror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const QUERY_PARAMS = [
'indentUnit',
'indentWithTab',
'language',
'lineComments',
'lineNumbers',
'lineSeparator',
'lineWrapping',
Expand Down
10 changes: 8 additions & 2 deletions app/templates/demo/code-mirror.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@
<Input @type="checkbox" @checked={{this.foldGutter}} />
<span class="ml-2">foldGutter</span>
</label>
<label class="{{labelClasses}}" title="Enable line comments">
<Input @type="checkbox" @checked={{this.lineComments}} />
<span class="ml-2">lineComments</span>
</label>
</codemirror-options-left>
<codemirror-options-right class="flex flex-wrap">
<label class="{{labelClasses}}" title="Enable visual line wrapping for lines exceeding editor width">
<Input @type="checkbox" @checked={{this.lineWrapping}} />
<span class="ml-2">lineWrapping</span>
Expand All @@ -175,8 +181,6 @@
<Input @type="checkbox" @checked={{this.scrollPastEnd}} />
<span class="ml-2">scrollPastEnd</span>
</label>
</codemirror-options-left>
<codemirror-options-right class="flex flex-wrap">
<label class="{{labelClasses}}" title="Limit maximum height of the component's element">
<Input @type="checkbox" @checked={{this.maxHeight}} />
<span class="ml-2">maxHeight</span>
Expand Down Expand Up @@ -345,6 +349,8 @@
@history={{this.history}}
@indentOnInput={{this.indentOnInput}}
@indentWithTab={{this.indentWithTab}}
@lineComments={{this.lineComments}}
@lineData={{if this.lineComments this.selectedDocument.lineData}}
@lineNumbers={{this.lineNumbers}}
@lineWrapping={{this.lineWrapping}}
@mergeControls={{this.mergeControls}}
Expand Down
33 changes: 33 additions & 0 deletions app/utils/code-mirror-documents.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
import { tracked } from '@glimmer/tracking';
import parseDiffAsDocument from 'codecrafters-frontend/utils/parse-diff-as-document';
import { LineData, LineDataCollection } from 'codecrafters-frontend/utils/code-mirror-line-comments';

function generateRandomLineData(linesCount = 0) {
function getRandomInt(inclusiveMin: number, exclusiveMax: number) {
const minCeiled = Math.ceil(inclusiveMin);
const maxFloored = Math.floor(exclusiveMax);

return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled);
}

const lineData = Array.from({ length: linesCount }).map(function (_v, lineNumber) {
const rnd = Math.random();

let commentsCount;

if (rnd < 0.05) {
commentsCount = getRandomInt(100, 1000);
} else if (rnd < 0.1) {
commentsCount = getRandomInt(10, 100);
} else if (rnd < 0.8) {
commentsCount = 0;
} else {
commentsCount = getRandomInt(1, 10);
}

return new LineData({ commentsCount, lineNumber: lineNumber + 1 });
});

return new LineDataCollection(lineData);
}

export class ExampleDocument {
@tracked document: string = '';
@tracked originalDocument: string;
@tracked filename: string;
@tracked language: string;
@tracked lineData?: LineDataCollection;

constructor({
document = '',
Expand All @@ -22,6 +53,8 @@ export class ExampleDocument {
this.originalDocument = originalDocument || document;
this.filename = filename;
this.language = language;

this.lineData = generateRandomLineData(document?.split('\n').length);
}

static createEmpty() {
Expand Down
54 changes: 54 additions & 0 deletions app/utils/code-mirror-line-comments-expanded-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Decoration, EditorView, ViewPlugin, ViewUpdate, type DecorationSet } from '@codemirror/view';
import { RangeSetBuilder, StateEffect } from '@codemirror/state';
import { expandedLineNumbersCompartment, expandedLineNumbersFacet } from 'codecrafters-frontend/utils/code-mirror-line-comments';

const lineCommentsExpandedDecoration = Decoration.line({
attributes: { class: 'cm-lineCommentsExpanded' },
});

function lineCommentsExpandedDecorations(view: EditorView) {
const expandedLines = view.state.facet(expandedLineNumbersFacet)[0] || [];
const builder = new RangeSetBuilder<Decoration>();

Check warning on line 11 in app/utils/code-mirror-line-comments-expanded-plugin.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-expanded-plugin.ts#L11

Added line #L11 was not covered by tests

for (const { from, to } of view.visibleRanges) {
for (let pos = from; pos <= to; ) {
const line = view.state.doc.lineAt(pos);

Check warning on line 15 in app/utils/code-mirror-line-comments-expanded-plugin.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-expanded-plugin.ts#L13-L15

Added lines #L13 - L15 were not covered by tests

if (expandedLines.includes(line.number)) {
builder.add(line.from, line.from, lineCommentsExpandedDecoration);

Check warning on line 18 in app/utils/code-mirror-line-comments-expanded-plugin.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-expanded-plugin.ts#L18

Added line #L18 was not covered by tests
}

pos = line.to + 1;

Check warning on line 21 in app/utils/code-mirror-line-comments-expanded-plugin.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-expanded-plugin.ts#L21

Added line #L21 was not covered by tests
}
}

return builder.finish();

Check warning on line 25 in app/utils/code-mirror-line-comments-expanded-plugin.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-expanded-plugin.ts#L25

Added line #L25 was not covered by tests
}

export function lineCommentsExpandedPlugin() {
return ViewPlugin.fromClass(

Check warning on line 29 in app/utils/code-mirror-line-comments-expanded-plugin.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-expanded-plugin.ts#L29

Added line #L29 was not covered by tests
class {
decorations: DecorationSet;

constructor(view: EditorView) {
this.decorations = lineCommentsExpandedDecorations(view);

Check warning on line 34 in app/utils/code-mirror-line-comments-expanded-plugin.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-expanded-plugin.ts#L34

Added line #L34 was not covered by tests
}

update(update: ViewUpdate) {
if (update.transactions) {
for (const tr of update.transactions) {
for (const effect of tr.effects) {

Check warning on line 40 in app/utils/code-mirror-line-comments-expanded-plugin.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-expanded-plugin.ts#L39-L40

Added lines #L39 - L40 were not covered by tests
if (effect instanceof StateEffect && effect.value?.compartment === expandedLineNumbersCompartment) {
this.decorations = lineCommentsExpandedDecorations(update.view);
break;

Check warning on line 43 in app/utils/code-mirror-line-comments-expanded-plugin.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-expanded-plugin.ts#L42-L43

Added lines #L42 - L43 were not covered by tests
}
}
}
}
}
},
{
decorations: (v) => v.decorations,

Check warning on line 51 in app/utils/code-mirror-line-comments-expanded-plugin.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-expanded-plugin.ts#L51

Added line #L51 was not covered by tests
},
);
}
118 changes: 118 additions & 0 deletions app/utils/code-mirror-line-comments-gutter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { BlockInfo, EditorView } from '@codemirror/view';
import { gutter as gutterRS, GutterMarker as GutterMarkerRS } from 'codecrafters-frontend/utils/code-mirror-gutter-rs';
import { expandedLineNumbersCompartment, expandedLineNumbersFacet, lineDataFacet } from 'codecrafters-frontend/utils/code-mirror-line-comments';

class CommentsCountGutterMarker extends GutterMarkerRS {
line: BlockInfo;

constructor(line: BlockInfo) {
super();
this.line = line;

Check warning on line 10 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L9-L10

Added lines #L9 - L10 were not covered by tests
}

toDOM(view: EditorView) {
const lineNumber = view.state.doc.lineAt(this.line.from).number;

Check warning on line 14 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L14

Added line #L14 was not covered by tests
const commentsCount = view.state.facet(lineDataFacet)[0]?.dataForLine(lineNumber)?.commentsCount || 0;
const elem = document.createElement('div');
const classNames = ['cm-commentsCount'];

Check warning on line 17 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L16-L17

Added lines #L16 - L17 were not covered by tests

if (commentsCount > 99) {
classNames.push('cm-commentsCountOver99');

Check warning on line 20 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L20

Added line #L20 was not covered by tests
}

elem.className = classNames.join(' ');

Check warning on line 23 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L23

Added line #L23 was not covered by tests
elem.innerText = `${commentsCount > 99 ? '99+' : commentsCount}`;

return elem;

Check warning on line 26 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L26

Added line #L26 was not covered by tests
}
}

class CommentButtonGutterMarker extends GutterMarkerRS {
line: BlockInfo;

constructor(line: BlockInfo) {
super();
this.line = line;

Check warning on line 35 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L34-L35

Added lines #L34 - L35 were not covered by tests
}

toDOM() {
const elem = document.createElement('div');
elem.className = 'cm-commentButton';

Check warning on line 40 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L39-L40

Added lines #L39 - L40 were not covered by tests

elem.innerText = `💬`;

Check warning on line 42 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L42

Added line #L42 was not covered by tests

return elem;

Check warning on line 44 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L44

Added line #L44 was not covered by tests
}
}

function lineCommentsGutterLineMarker(view: EditorView, line: BlockInfo) {
const lineNumber = view.state.doc.lineAt(line.from).number;

Check warning on line 49 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L49

Added line #L49 was not covered by tests
const commentsCount = view.state.facet(lineDataFacet)[0]?.dataForLine(lineNumber)?.commentsCount || 0;

return new (commentsCount === 0 ? CommentButtonGutterMarker : CommentsCountGutterMarker)(line);
}

function lineCommentsGutterClickHandler(view: EditorView, line: BlockInfo) {
const lineNumber = view.state.doc.lineAt(line.from).number;

Check warning on line 56 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L56

Added line #L56 was not covered by tests
const expandedLines = view.state.facet(expandedLineNumbersFacet)[0] || [];
const newExpandedLines = expandedLines.includes(lineNumber) ? expandedLines.without(lineNumber) : [...expandedLines, lineNumber];

view.dispatch({

Check warning on line 60 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L60

Added line #L60 was not covered by tests
effects: [expandedLineNumbersCompartment.reconfigure(expandedLineNumbersFacet.of(newExpandedLines))],
});

return true;

Check warning on line 64 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L64

Added line #L64 was not covered by tests
}

const lineCommentsGutterBaseTheme = EditorView.baseTheme({
'.cm-lineCommentsGutter': {
minWidth: '24px',
textAlign: 'center',
userSelect: 'none',

'& .cm-gutterElement': {
cursor: 'pointer',

'& .cm-commentButton': {
opacity: '0.15',
},

'& .cm-commentsCount': {
display: 'block',
backgroundColor: '#ffcd72c0',
borderRadius: '50%',
color: '#24292e',
transform: 'scale(0.75)',
fontWeight: '500',
fontSize: '12px',

'&.cm-commentsCountOver99': {
fontSize: '9.5px',
},
},

'&:hover': {
'& .cm-commentButton': {
opacity: '1',
},

'& .cm-commentsCount': {
backgroundColor: '#ffa500',
},
},
},
},
});

export function lineCommentsGutter() {
return [

Check warning on line 108 in app/utils/code-mirror-line-comments-gutter.ts

View check run for this annotation

Codecov / codecov/patch

app/utils/code-mirror-line-comments-gutter.ts#L108

Added line #L108 was not covered by tests
gutterRS({
class: 'cm-lineCommentsGutter',
lineMarker: lineCommentsGutterLineMarker,
domEventHandlers: {
click: lineCommentsGutterClickHandler,
},
}),
lineCommentsGutterBaseTheme,
];
}
Loading
Loading