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 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
717b6a5
[WIP] Add `lineComments` CodeMirror extension
VasylMarchuk Jan 17, 2025
19fe670
[WIP] Add `lineComments` CodeMirror extension - external comments
VasylMarchuk Jan 18, 2025
a1ffeae
[WIP] Add `lineComments` CodeMirror extension - lineDataCollection
VasylMarchuk Jan 19, 2025
79e5bea
[WIP] Add `lineComments` CodeMirror extension - `lineCommentsGutter` …
VasylMarchuk Jan 19, 2025
4bf06d2
[WIP] Add `lineComments` CodeMirror extension - `lineCommentsWidget` …
VasylMarchuk Jan 21, 2025
a4956e7
[WIP] Add `lineComments` CodeMirror extension - proper classNames in …
VasylMarchuk Jan 21, 2025
3a4a910
[WIP] Add `lineComments` CodeMirror extension - toggling of lineComme…
VasylMarchuk Jan 21, 2025
8a513c1
[WIP] Add `lineComments` CodeMirror extension - highlightActiveLineRS…
VasylMarchuk Jan 21, 2025
3de83fb
[WIP] Add `lineComments` CodeMirror extension - lineCommentsClickHandler
VasylMarchuk Jan 24, 2025
5d8d9da
[WIP] Add `lineComments` CodeMirror extension - lineCommentsExpandedP…
VasylMarchuk Jan 26, 2025
c012f41
[WIP] Generate dummy comments for example documents
VasylMarchuk Apr 20, 2025
b601663
[WIP] Convert widget to block form to fix cursor position at EOL
VasylMarchuk May 4, 2025
485d746
[WIP] Fix flickering of expanded comments when making inline edits
VasylMarchuk May 4, 2025
db2d5e4
[WIP] Disable line comments when `@readOnly=false`
VasylMarchuk May 4, 2025
b3f516f
[WIP] Better logic for resetting option compartments
VasylMarchuk Jun 21, 2025
b377410
[WIP] Better logic for resetting `originalDocumentOrDiffRelatedOption…
VasylMarchuk Jun 21, 2025
874ed41
[WIP] Better constructors
VasylMarchuk Jun 21, 2025
6952903
[WIP] Better comment for when `lineComments` are disabled
VasylMarchuk Jun 21, 2025
2f40de3
[WIP] Fix template lint error
VasylMarchuk Jun 21, 2025
0317e16
Hide line comments for last lines in a collapsed group
VasylMarchuk Jun 22, 2025
87d0995
Hide line comments for last lines in a collapsed range group
VasylMarchuk Jul 2, 2025
dcbcb5f
Switch `lineCommentsGutter` to using native right-side gutter
VasylMarchuk Jul 3, 2025
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
5 changes: 4 additions & 1 deletion app/components/code-mirror.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<code-mirror
{{did-insert this.renderEditor}}
{{! Do not sort did-updates alphabetically, their order matters }}
{{did-update this.documentDidChange @document}}
{{did-update this.optionDidChange "allowMultipleSelections" @allowMultipleSelections}}
{{did-update this.optionDidChange "autocompletion" @autocompletion}}
Expand All @@ -24,9 +25,11 @@
{{did-update this.optionDidChange "indentWithTab" @indentWithTab}}
{{did-update this.optionDidChange "languageOrFilename" @filename}}
{{did-update this.optionDidChange "languageOrFilename" @language}}
{{did-update this.optionDidChange "lineCommentsOrCommentsRelatedOption" (array @lineComments @lineData @readOnly)}}
{{did-update this.optionDidChange "lineNumbers" @lineNumbers}}
{{did-update this.optionDidChange "lineSeparator" @lineSeparator}}
{{did-update this.optionDidChange "lineWrapping" @lineWrapping}}
{{did-update this.optionDidChange "syntaxHighlighting" @syntaxHighlighting}}
{{did-update
this.optionDidChange
"originalDocumentOrDiffRelatedOption"
Expand All @@ -37,6 +40,7 @@
@collapseUnchanged
@highlightChanges
@syntaxHighlightDeletions
@syntaxHighlighting
@unchangedMargin
@unchangedMinSize
@allowInlineDiffs
Expand All @@ -46,7 +50,6 @@
{{did-update this.optionDidChange "readOnly" @readOnly}}
{{did-update this.optionDidChange "rectangularSelection" @rectangularSelection}}
{{did-update this.optionDidChange "scrollPastEnd" @scrollPastEnd}}
{{did-update this.optionDidChange "syntaxHighlighting" @syntaxHighlighting}}
{{did-update this.optionDidChange "tabSize" @tabSize}}
{{did-update this.optionDidChange "theme" @theme}}
data-test-code-mirror-component
Expand Down
36 changes: 15 additions & 21 deletions app/components/code-mirror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { collapseRanges } from 'codecrafters-frontend/utils/code-mirror-collapse
import { collapseRangesGutter } from 'codecrafters-frontend/utils/code-mirror-collapse-ranges-gutter';
import { collapseUnchanged } from 'codecrafters-frontend/utils/code-mirror-collapse-unchanged';
import { collapseUnchangedGutter } from 'codecrafters-frontend/utils/code-mirror-collapse-unchanged-gutter';
import { lineComments, type LineDataCollection } from 'codecrafters-frontend/utils/code-mirror-line-comments';

function generateHTMLElement(src: string): HTMLElement {
const div = document.createElement('div');
Expand All @@ -59,7 +60,7 @@ export type LineRange = { startLine: number; endLine: number };

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

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

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

Expand All @@ -86,6 +87,8 @@ 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])] : []),
lineCommentsOrCommentsRelatedOption: ({ lineComments: enabled, lineData, readOnly }) =>
enabled && lineData && readOnly ? [lineComments(lineData)] : [],
lineNumbers: ({ lineNumbers: enabled }) => (enabled ? [lineNumbers()] : []),
foldGutter: ({ foldGutter: enabled }) =>
enabled
Expand Down Expand Up @@ -270,6 +273,14 @@ export interface Signature {
* Explicitly pass a language to the editor
*/
language?: string;
/**
* Enable line comments (disabled when document is not read-only)
*/
lineComments?: boolean;
/**
* Line data containing comments counts or other line-related metadata
*/
lineData?: LineDataCollection;
/**
* Enable the line numbers gutter
*/
Expand Down Expand Up @@ -390,17 +401,10 @@ export default class CodeMirrorComponent extends Component<Signature> {
return;
}

// When originalDocument changes - completely unload the diff compartment to avoid any side-effects
if (optionName === 'originalDocumentOrDiffRelatedOption') {
this.#updateRenderedView({
effects: this.#resetCompartment('originalDocumentOrDiffRelatedOption'),
});
}

// When collapsedRanges changes - completely unload the collapsedRanges compartment to avoid any side-effects
if (optionName === 'collapsedRanges') {
// These options need to be reset after changing, to avoid any side-effects
if (['collapsedRanges', 'lineCommentsOrCommentsRelatedOption', 'originalDocumentOrDiffRelatedOption'].includes(optionName)) {
this.#updateRenderedView({
effects: this.#resetCompartment('collapsedRanges'),
effects: this.#resetCompartment(optionName),
});
}

Expand All @@ -409,16 +413,6 @@ export default class CodeMirrorComponent extends Component<Signature> {
effects: await this.#updateCompartment(optionName),
});

// When syntaxHighlighting changes - reload the diff compartment to also re-configure syntaxHighlightDeletions
if (optionName === 'syntaxHighlighting') {
this.#updateRenderedView({
effects: this.#resetCompartment('originalDocumentOrDiffRelatedOption'),
});
this.#updateRenderedView({
effects: await this.#updateCompartment('originalDocumentOrDiffRelatedOption'),
});
}

// When lineSeparator changes - completely reload the document to avoid any side-effects
if (optionName === 'lineSeparator') {
this.#updateRenderedView(
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 @@ -57,6 +57,7 @@ const OPTION_DEFAULTS = {
indentWithTab: true,
language: true,
lineNumbers: true,
lineComments: false,
lineSeparator: true,
lineWrapping: true,
maxHeight: true,
Expand Down Expand Up @@ -114,6 +115,7 @@ export default class DemoCodeMirrorController extends Controller {
'indentUnit',
'indentWithTab',
'language',
'lineComments',
'lineNumbers',
'lineSeparator',
'lineWrapping',
Expand Down Expand Up @@ -176,6 +178,7 @@ export default class DemoCodeMirrorController extends Controller {
@tracked indentUnit = OPTION_DEFAULTS.indentUnit;
@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 @@ -28,6 +28,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 @@ -179,6 +179,12 @@
<Input @type="checkbox" @checked={{this.foldGutter}} />
<span class="ml-2">foldGutter</span>
</label>
<label class="{{labelClasses}}" title="Enable line comments (disabled when document is not read-only)">
<Input @type="checkbox" @checked={{this.lineComments}} disabled={{not this.readOnly}} />
<span class="ml-2 {{unless this.readOnly 'text-gray-300'}}">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 @@ -187,8 +193,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 @@
@indentUnit={{if this.indentUnit this.selectedIndentUnit.symbol}}
@indentWithTab={{this.indentWithTab}}
@language={{if this.language this.selectedDocument.language}}
@lineComments={{this.lineComments}}
@lineData={{if this.lineComments this.selectedDocument.lineData}}
@lineNumbers={{this.lineNumbers}}
@lineSeparator={{if this.lineSeparator this.selectedLineSeparator.symbol}}
@lineWrapping={{this.lineWrapping}}
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,6 +1,36 @@
import { tracked } from '@glimmer/tracking';
import type { LineRange } from 'codecrafters-frontend/components/code-mirror';
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(lineNumber + 1, commentsCount);
});

return new LineDataCollection(lineData);
}

export class ExampleDocument {
@tracked document: string = '';
Expand All @@ -9,6 +39,7 @@ export class ExampleDocument {
@tracked language: string;
@tracked highlightedRanges: LineRange[];
@tracked collapsedRanges: LineRange[];
@tracked lineData?: LineDataCollection;

constructor({
document = '',
Expand All @@ -31,6 +62,8 @@ export class ExampleDocument {
this.language = language;
this.highlightedRanges = highlightedRanges;
this.collapsedRanges = collapsedRanges;

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

static createEmpty() {
Expand Down
64 changes: 64 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,64 @@
import { Decoration, EditorView, ViewPlugin, ViewUpdate, type DecorationSet } from '@codemirror/view';
import { RangeSetBuilder } from '@codemirror/state';
import { 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 (let i = 0; i < view.state.doc.lines; i++) {
// const line = view.state.doc.line(i + 1);

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

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

Check warning on line 23 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-L23

Added lines #L21 - L23 were not covered by tests

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

Check warning on line 26 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#L26

Added line #L26 was not covered by tests
}

pos = line.to + 1;

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
}
}

return builder.finish();

Check warning on line 33 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#L33

Added line #L33 was not covered by tests
}

export function lineCommentsExpandedPlugin() {
return ViewPlugin.fromClass(

Check warning on line 37 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#L37

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

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

Check warning on line 42 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

Added line #L42 was not covered by tests
}

update(update: ViewUpdate) {
this.decorations = lineCommentsExpandedDecorations(update.view);

Check warning on line 46 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#L46

Added line #L46 was not covered by tests

// if (update.transactions) {
// for (const tr of update.transactions) {
// for (const effect of tr.effects) {
// if (effect instanceof StateEffect && effect.value?.compartment === expandedLineNumbersCompartment) {
// this.decorations = lineCommentsExpandedDecorations(update.view);
// break;
// }
// }
// }
// }
}
},
{
decorations: (v) => v.decorations,

Check warning on line 61 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#L61

Added line #L61 was not covered by tests
},
);
}
Loading