-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
627 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { commentStateEffect } from "./textareaWidget"; | ||
import { commentMarker } from "../comments/sidebarWidget"; | ||
import { ycommentsFacet } from "./state"; | ||
import { EditorView } from "@codemirror/view"; | ||
|
||
const commentExtension = ycomments => [ | ||
ycommentsFacet.of(ycomments), | ||
commentStateEffect, | ||
commentMarker, | ||
EditorView.updateListener.of(update => ycomments.syncComments(update)) | ||
] | ||
|
||
export { commentExtension } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { gutter, GutterMarker, BlockInfo, EditorView } from "@codemirror/view" | ||
import { YComments } from './ycomments'; | ||
import { updateShownComments, ycommentsFacet } from "./state"; | ||
|
||
class CommentMarker extends GutterMarker { | ||
static MAIN_CLASS = "comment-gutter"; | ||
static ICON_CLASS = "comment-gutter-icon"; | ||
static HAS_COMMENTS_CLASS = "has-comments"; | ||
|
||
/** | ||
* @param {BlockInfo} line | ||
* @param {EditorView} view | ||
*/ | ||
constructor(line, view) { | ||
super(); | ||
this.line = line; | ||
this.view = view; | ||
if (view) { | ||
this.ycomments = this.view.state.facet(ycommentsFacet); | ||
this.lineNumber = this.view.state.doc.lineAt(this.line.to).number; | ||
} | ||
} | ||
|
||
gutterCommentMarker() { | ||
const commentMarker = document.createElement("div"); | ||
commentMarker.classList.add(CommentMarker.MAIN_CLASS) | ||
if (this.lineNumber) { | ||
commentMarker.style.width = (this.lineNumber.toString().length * 5) + "px"; | ||
} | ||
return commentMarker | ||
} | ||
|
||
hasComments() { | ||
if (!this.line) { | ||
return false; | ||
} | ||
|
||
this.commentId = this.ycomments | ||
.iterYComments() | ||
.find(({lineNumber}) => lineNumber == this.lineNumber) | ||
?.commentId; | ||
|
||
return Boolean(this.commentId); | ||
} | ||
|
||
popupIcon() { | ||
const icon = document.createElement("section"); | ||
icon.classList = CommentMarker.ICON_CLASS; | ||
return icon; | ||
} | ||
|
||
markHasComments(marker) { | ||
marker.classList.add(CommentMarker.HAS_COMMENTS_CLASS); | ||
} | ||
|
||
addPopupIcon(marker) { | ||
marker.appendChild(this.popupIcon()) | ||
} | ||
|
||
/** Main function. Used to render the actual gutter marker */ | ||
toDOM() { | ||
const marker = this.gutterCommentMarker(); | ||
if (this.hasComments()) { | ||
this.markHasComments(marker) | ||
} | ||
this.addPopupIcon(marker); | ||
return marker | ||
} | ||
} | ||
|
||
/** | ||
* @param {YComments} ycomments | ||
* @param {EditorView} view | ||
*/ | ||
const getOrCreateComment = (view, line, ycomments) => { | ||
const thisLineNumber = view.state.doc.lineAt(line.to).number; | ||
const comment = ycomments.iterYComments() | ||
.find(({lineNumber}) => lineNumber == thisLineNumber) | ||
?.commentId | ||
|
||
if (!comment) { | ||
return ycomments.newComment(thisLineNumber); | ||
} | ||
|
||
return comment | ||
} | ||
|
||
/** @type {import("@codemirror/state").Extension} */ | ||
const commentMarker = gutter({ | ||
lineMarker(view, line) { | ||
return new CommentMarker(line, view) | ||
}, | ||
lineMarkerChange: (update) => update.transactions.some(t => t.effects.some(e => e.is(updateShownComments))), | ||
initialSpacer: () => { return new CommentMarker(null, null) }, | ||
domEventHandlers: { | ||
mousedown(view, line) { | ||
let ycomments = view.state.facet(ycommentsFacet.reader); | ||
let commentId = getOrCreateComment(view, line, ycomments); | ||
let willBeVisible = ycomments.switchVisibility(commentId); | ||
|
||
if (!willBeVisible && ycomments.isEmpty(commentId)) { | ||
ycomments.deleteComment(commentId); | ||
} | ||
|
||
view.dispatch({ effects: updateShownComments.of(null) }); | ||
} | ||
} | ||
}) | ||
|
||
|
||
export { commentMarker } | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { YComments } from './ycomments'; | ||
import { Facet, StateEffect } from "@codemirror/state"; | ||
|
||
/** @type {Facet<YComments, YComments>} */ | ||
const ycommentsFacet = Facet.define({ | ||
combine: values => values[values.length - 1], | ||
static: true | ||
}) | ||
|
||
/** @type {StateEffect<null>} */ | ||
const updateShownComments = StateEffect.define(); | ||
|
||
export { | ||
ycommentsFacet, | ||
updateShownComments | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { EditorView } from "@codemirror/view" | ||
import { Decoration, WidgetType } from "@codemirror/view" | ||
import { RangeSetBuilder, RangeSet, StateField, Transaction } from "@codemirror/state"; | ||
import { ycommentsFacet, updateShownComments, } from "./state"; | ||
import { YComments } from "./ycomments"; | ||
|
||
/** A placeholder. The role of this element is to reserve enough vertical and horizontal space for real comment. | ||
* The real comment HTML element is rendered on the top of the textarea, outside the DOM tree of Code Mirror. | ||
*/ | ||
class Comment extends WidgetType { | ||
constructor(height, commentId) { | ||
super(); | ||
this.height = height; | ||
this.commentId = commentId; | ||
} | ||
|
||
toDOM() { | ||
const btn = document.createElement("div"); | ||
btn.id = this.commentId; | ||
btn.classList = "comment-box"; | ||
btn.style.height = (this.height) + "px"; | ||
return btn | ||
} | ||
} | ||
|
||
const commentWidget = (height, commentId) => Decoration.widget({ widget: new Comment(height, commentId), side: 10000, inlineOrder: false, block: true }) | ||
|
||
const sortByLine = (commentA, commentB) => commentA.lineNumber - commentB.lineNumber; | ||
|
||
/** | ||
* @param {Transaction} transaction | ||
* @returns {RangeSet<Decoration>} | ||
*/ | ||
const shouldUpdateTextWidget = (transaction) => | ||
transaction.docChanged || | ||
transaction | ||
.effects | ||
.some(eff => eff.is(updateShownComments)); | ||
|
||
/** @param {Transaction} transaction */ | ||
const buildTextareaWidgets = (transaction) => [ | ||
(builder, { commentId, lineNumber, height }) => { | ||
try { | ||
const mountPoint = transaction.newDoc.line(lineNumber).to | ||
builder.add(mountPoint, mountPoint, commentWidget(height, commentId)); | ||
} catch (e) { | ||
console.warn(e) | ||
console.warn(`An error occured when rendering comment ${commentId}. Comment will not be shown.`) | ||
} | ||
return builder; | ||
}, | ||
new RangeSetBuilder() | ||
]; | ||
|
||
/** | ||
* @param {Transaction} transaction | ||
* @param {YComments} ycomments | ||
*/ | ||
const moveComments = (transaction, ycomments) => { | ||
if (transaction.isUserEvent("input") || transaction.isUserEvent("delete")) { | ||
const lineDiff = transaction.state.doc.lines - transaction.startState.doc.lines | ||
if (lineDiff != 0) { | ||
const docLines = transaction.state.doc.lines; | ||
const cursorLine = transaction.state.doc.lineAt(transaction.selection.main.from).number - lineDiff; | ||
ycomments.moveComments(cursorLine, lineDiff, docLines); | ||
} | ||
} | ||
} | ||
|
||
/** @type {StateField<RangeSet<Decoration>>} */ | ||
const commentStateEffect = StateField.define({ | ||
|
||
/** | ||
* @returns {RangeSet<Decoration>} | ||
*/ | ||
create() { | ||
return new RangeSetBuilder().finish() | ||
}, | ||
|
||
/** | ||
* @param {RangeSet<Decoration>} oldState | ||
* @param {Transaction} transaction | ||
* @returns {RangeSet<Decoration>} | ||
*/ | ||
update(oldState, transaction) { | ||
if (shouldUpdateTextWidget(transaction)) { | ||
const ycomments = transaction.state.facet(ycommentsFacet); | ||
const isShown = ({isShown}) => isShown; | ||
|
||
moveComments(transaction, ycomments) | ||
|
||
return ycomments | ||
.iterComments() | ||
.filter(isShown) | ||
.sort(sortByLine) | ||
.reduce( | ||
...buildTextareaWidgets(transaction) | ||
) | ||
.finish() | ||
} | ||
|
||
return oldState | ||
}, | ||
|
||
provide(field) { | ||
return EditorView.decorations.from(field); | ||
} | ||
}); | ||
|
||
export { commentStateEffect } | ||
|
Oops, something went wrong.