Skip to content

Commit

Permalink
[#55699] Add suggest changes button
Browse files Browse the repository at this point in the history
  • Loading branch information
Trzcin authored and mgielda committed Aug 19, 2024
1 parent a1d5df5 commit a2d13f7
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 5 deletions.
4 changes: 3 additions & 1 deletion src/comments/lineAuthors.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const lineAuthorsFacet = Facet.define({

////////////////////////////// LINE-COLORING //////////////////////////////

export const lineAuthorsEffect = StateEffect.define();

/**
* A CodeMirror extension. Colors line of the editor respective to their authors.
*
Expand Down Expand Up @@ -104,7 +106,7 @@ const commentLineHighlighter = ViewPlugin.fromClass(

/** @param {ViewUpdate} update */
update(update) {
if (update.docChanged || update.viewportChanged) {
if (update.docChanged || update.viewportChanged || update.transactions.some((t) => t.effects.some((e) => e.is(lineAuthorsEffect)))) {
update.transactions.filter(isUserEvent).forEach((t) => this.markLinesEditedInTransaction(t));

this.decorations = this.colorEditorLines(update.view);
Expand Down
24 changes: 24 additions & 0 deletions src/comments/ycomments.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,11 @@ export class YComments {
this.draggedComment = null;
this.commentWithPopup = null;

/** commentId -> (EditorView) => void - here a listener can be added to wait for an editor for a comment to become available */
this.commentEditorsListeners = new Map();
/** commentId -> CodeMirror View */
this.commentEditors = new Map();

this.suggestions = ydoc.getMap("suggestions");
this.suggestions.observe(() => {
if (!this.mainCodeMirror) return;
Expand Down Expand Up @@ -555,4 +560,23 @@ export class YComments {
this.display().setVisibility(id, true);
}
}

registerCommentEditor(commentId, view) {
this.commentEditors.set(commentId, view);
if (this.commentEditorsListeners.has(commentId)) {
this.commentEditorsListeners.get(commentId)(view);
this.commentEditorsListeners.delete(commentId);
}
}

getEditorForComment(commentId) {
return new Promise((resolve) => {
if (this.commentEditors.has(commentId)) {
resolve(this.commentEditors.get(commentId));
} else {
// Wait for the editor to be available, then resolve.
this.commentEditorsListeners.set(commentId, resolve);
}
});
}
}
8 changes: 7 additions & 1 deletion src/components/CodeMirror.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { ExtensionBuilder } from "../extensions";
import { YCommentsParent } from "../components/Comment";
import commentIcon from "../icons/comment.svg?raw";
import { customHighlighter } from "../extensions/customHighlights";
import { suggestionCompartment } from "../extensions/suggestions";
import { AddSuggestionBtn, suggestionCompartment } from "../extensions/suggestions";
import editIcon from "../icons/edit.svg";

const CodeEditor = styled.div`
border-radius: var(--border-radius);
Expand Down Expand Up @@ -203,6 +204,7 @@ const CodeMirror = ({ text, id, name, mode, spellcheckOpts, highlights, collabor
editorRef,
})
.useComments({ enabled: collaboration.opts.commentsEnabled, ycomments: collaboration.ycomments })
.useSuggestionPopup({ enabled: collaboration.opts.commentsEnabled, ycomments: collaboration.ycomments })
.addUpdateListener((update) => update.docChanged && text.set(view.state.doc.toString()))
.useRemoveSelectionOnBlur()
.create(),
Expand Down Expand Up @@ -233,6 +235,10 @@ const CodeMirror = ({ text, id, name, mode, spellcheckOpts, highlights, collabor
${collaboration.opts.commentsEnabled &&
!collaboration.error &&
html`<${YCommentsParent} ycomments=${collaboration.ycomments} collaboration=${collaboration.opts} />`}
${collaboration.opts.commentsEnabled &&
html`<${AddSuggestionBtn} style="display: none" className="myst-add-suggestion" title="Suggest Changes">
<img src=${editIcon} alt="edit" />
<//>`}
<//>
<${HiddenTextArea} value=${text.get()} name=${name} id=${id}><//>
`;
Expand Down
1 change: 1 addition & 0 deletions src/components/Comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const YComment = ({ ycomments, commentId, collaboration }) => {
});

ycomments.syncSuggestions(commentId);
ycomments.registerCommentEditor(commentId, view);
ytext.observe((_, tr) => {
if (!tr.local) return;
ycomments.syncSuggestions(commentId);
Expand Down
8 changes: 8 additions & 0 deletions src/extensions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import spellcheck from "./spellchecker";
import { customHighlighter } from "./customHighlights";
import { commentExtension } from "../comments";
import { commentAuthoring } from "../comments/lineAuthors";
import { suggestionPopup } from "./suggestions";

const basicSetupWithoutHistory = basicSetup.filter((_, i) => i != 3);
const minimalSetupWithoutHistory = minimalSetup.filter((_, i) => i != 1);
Expand Down Expand Up @@ -136,6 +137,13 @@ export class ExtensionBuilder {
return this;
}

useSuggestionPopup({ enabled, ycomments }) {
if (enabled) {
this.base.push(EditorView.updateListener.of((update) => suggestionPopup(update, ycomments)));
}
return this;
}

create() {
return [...this.important, ...this.base, ...this.extensions];
}
Expand Down
109 changes: 106 additions & 3 deletions src/extensions/suggestions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Compartment, StateEffect } from "@codemirror/state";
import { Decoration, WidgetType } from "@codemirror/view";
import { Compartment, EditorSelection, StateEffect } from "@codemirror/state";
import { Decoration, EditorView, ViewUpdate, WidgetType } from "@codemirror/view";
import { YComments } from "../comments/ycomments";
import { lineAuthorsEffect } from "../comments/lineAuthors";
import styled from "styled-components";
import DefaultButton from "../components/Buttons";

export const suggestionEffect = StateEffect.define();

Expand Down Expand Up @@ -27,7 +31,7 @@ export function parseCommentLine({ commentId, text, color }) {

if (targetStr.length !== 0) {
suggestions.push({
targetRegexSrc: `(?<=^|[ \\t\\r\\.]|\\b)${escapeRegExp(targetStr)}(?=$|[\\s\\.]|\\b)`,
targetRegexSrc: `(?<=^|[ \\t\\r\\.]|\\W)${escapeRegExp(targetStr)}(?=$|[\\s\\.]|\\W)`,
targetRegexFlags: "gm",
id: commentId,
cssClass: "cm-suggestion",
Expand Down Expand Up @@ -97,3 +101,102 @@ class Replacement extends WidgetType {
return replacementText;
}
}

export function suggestionPopup(/** @type {ViewUpdate} */ update, /** @type {YComments} */ ycomments) {
const addSuggestionBtn = document.querySelector(".myst-add-suggestion");
const mainSel = update.state.selection.main;
const noSelection = mainSel.head === mainSel.anchor;
const multilineSelection = update.state.doc.lineAt(mainSel.head).number !== update.state.doc.lineAt(mainSel.anchor).number;
if (!update.selectionSet || noSelection || multilineSelection) {
addSuggestionBtn.style.display = "none";
return;
}

const startPos = update.view.coordsAtPos(mainSel.from);
const endPos = update.view.coordsAtPos(mainSel.to);
const middle = (startPos.left + endPos.left) / 2;
addSuggestionBtn.style.top = `${startPos.top - 130 + window.scrollY}px`;
addSuggestionBtn.style.left = `${middle - 20}px`;
addSuggestionBtn.style.display = "block";

addSuggestionBtn.onmousedown = async (ev) => {
// This is to ensure the button does not take focus from CodeMirror
ev.preventDefault();
const line = update.state.doc.lineAt(mainSel.from);

let suggestionFrom = mainSel.from;
const wordBoundaryRegex = /[ \t\r\W]/;
let pos;
for (pos = suggestionFrom - 1; pos >= line.from; pos--) {
if (wordBoundaryRegex.test(update.state.doc.sliceString(pos, pos + 1))) {
break;
}
}
suggestionFrom = pos + 1;

let suggestionTo = mainSel.to;
for (pos = suggestionTo; pos <= line.to; pos++) {
if (wordBoundaryRegex.test(update.state.doc.sliceString(pos, pos + 1))) {
break;
}
}
suggestionTo = pos;

let suggestionText = `|${update.state.doc.sliceString(suggestionFrom, suggestionTo)} -> |`;
let id = ycomments.findCommentOn(line.number)?.commentId;
if (id) {
suggestionText = "\n" + suggestionText;
} else {
id = ycomments.newComment(line.number);
}

ycomments.ydoc.transact(() => {
const text = ycomments.getTextForComment(id);
text.insert(text.length, suggestionText);
const authors = ycomments.lineAuthors(id);
authors.mark(authors.lineAuthors.length);
}, ycomments.provider.awareness.clientID);

ycomments.display().setVisibility(id, true);
ycomments.updateMainCodeMirror();

/** @type {EditorView} */
const commentView = await ycomments.getEditorForComment(id);
commentView.focus();
commentView.dispatch({
selection: EditorSelection.create([EditorSelection.range(commentView.state.doc.length - 1, commentView.state.doc.length - 1)]),
effects: lineAuthorsEffect.of(null),
});
};
}

export const AddSuggestionBtn = styled(DefaultButton)`
position: absolute;
z-index: 10;
display: none;
transform: translateX(-50%);
margin: 0 !important;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 !important;
&:hover {
background-color: var(--gray-400);
}
img {
filter: invert(100%);
}
&::after {
content: "";
position: absolute;
display: block;
transform: translate(10px, 8px);
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid var(--icon-border);
}
`;
3 changes: 3 additions & 0 deletions src/icons/edit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit a2d13f7

Please sign in to comment.