Skip to content
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

Communication: Fix improper deletion of combined emojis in Monaco editor #10242

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from

Conversation

asliayk
Copy link
Contributor

@asliayk asliayk commented Feb 1, 2025

Checklist

General

Client

Motivation and Context

Currently, deleting a combined emoji (e.g., 🇩🇪) leaves behind a partial character instead of fully removing it. (Closes #9944)

Description

This issue has been fixed using grapheme-splitter. The backspace command in Monaco editor now detects and removes entire grapheme clusters, ensuring proper deletion of multi-character emojis.

Steps for Testing

Prerequisites:

  • 1 User
  • 1 Course with Communication enabled
  1. Log in to Artemis.
  2. Navigate to the Communication section of a course.
  3. Open any channel and type a combined emoji (e.g., 🇩🇪).
  4. Press Backspace to delete the emoji and verify that it is fully removed without leaving a partial character.

Testserver States

Note

These badges show the state of the test servers.
Green = Currently available, Red = Currently locked
Click on the badges to get to the test servers.







Review Progress

Code Review

  • Code Review 1
  • Code Review 2

Manual Tests

  • Test 1
  • Test 2

Test Coverage

Client

Class/File Line Coverage Confirmation (assert/expect)
emoji.action.ts 97.56% ✅ ❌
monaco-editor.component.ts 93.82% ✅ ❌

Summary by CodeRabbit

  • New Features
    • Enhanced our text editor for a smarter emoji experience. Emoji insertion now dynamically adjusts the cursor based on the actual emoji length, ensuring accurate placement. In addition, the backspace key now intuitively removes complete emoji clusters instead of single characters. These updates deliver a smoother and more natural text editing process when working with emojis.

@asliayk asliayk added tests client Pull requests that update TypeScript code. (Added Automatically!) bugfix component:Communication labels Feb 1, 2025
@asliayk asliayk self-assigned this Feb 1, 2025
@asliayk asliayk temporarily deployed to artemis-test4.artemis.cit.tum.de February 1, 2025 15:14 — with GitHub Actions Inactive
@asliayk asliayk marked this pull request as ready for review February 1, 2025 15:24
@asliayk asliayk requested a review from a team as a code owner February 1, 2025 15:24
Copy link

coderabbitai bot commented Feb 1, 2025

Walkthrough

This pull request introduces a new dependency, grapheme-splitter, into the project and updates the emoji handling logic in the Monaco editor. It adjusts the insertion of emojis by calculating the cursor position based on the emoji string's length. Additionally, the PR implements a custom backspace command for improved deletion of graphemes, including combined emojis, and adds new tests to verify both insertion and deletion functionalities.

Changes

File(s) Change Summary
package.json Added new dependency "grapheme-splitter": "^1.0.4".
src/.../monaco-editor/model/actions/emoji.action.ts Updated insertEmojiAtCursor method to calculate the new cursor position based on the inserted emoji's length.
src/.../monaco-editor/monaco-editor.component.ts Imported GraphemeSplitter; added a custom backspace command in ngOnInit to handle deletion of graphemes; introduced a private customBackspaceCommandId property and a public getCustomBackspaceCommandId method; adjusted the cursor update logic in setText.
src/test/.../postings-markdown-editor.component.spec.ts
src/test/.../monaco-editor.component.spec.ts
Added test cases for verifying emoji insertion behavior and correct deletion of combined emojis, including adjustment of cursor position after deletion.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Editor
    participant EmojiAction
    User->>Editor: Open emoji picker
    Editor->>EmojiAction: Request emoji insertion at cursor
    Note right of EmojiAction: Calculate new cursor using emoji length
    EmojiAction-->>Editor: Insert emoji and update cursor
Loading
sequenceDiagram
    participant User
    participant Editor
    participant MonacoComponent
    participant GraphemeSplitter
    User->>Editor: Press Backspace key
    Editor->>MonacoComponent: Invoke custom backspace command
    MonacoComponent->>GraphemeSplitter: Split current line into grapheme clusters
    GraphemeSplitter-->>MonacoComponent: Return grapheme clusters
    MonacoComponent-->>Editor: Delete appropriate grapheme and update cursor
Loading

Assessment against linked issues

Objective Addressed Explanation
Full deletion of combined emojis [#9944]

Possibly related PRs

Suggested labels

ready to merge, lock:artemis-test4

Suggested reviewers

  • HawKhiem
  • sachmii
  • Feras797
  • JerroyTan
  • krusche
  • anian03
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai or @coderabbitai title anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/main/webapp/app/shared/monaco-editor/model/actions/emoji.action.ts (1)

97-97: LGTM! Improved cursor positioning for emojis.

The change correctly uses the emoji's actual length instead of a fixed value, which is more accurate for combined emojis like flags (e.g., 🇩🇪).

Consider adding a comment explaining why we use emoji.length for cursor positioning, as it might not be immediately obvious that some emojis can be longer than others:

+        // Use emoji.length to handle combined emojis (e.g., flags) which can be longer than 2 characters
         const newPosition = new TextEditorPosition(position.getLineNumber(), position.getColumn() + emoji.length);
src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts (1)

349-376: Enhance test readability with descriptive assertions.

The test effectively verifies sequential deletion of combined emojis, but could be more readable with descriptive assertion messages.

Consider adding descriptive messages to assertions:

-        expect(comp.getText()).toEqual(emoji1);
+        expect(comp.getText()).toEqual(emoji1, 'Should retain first emoji after deleting second emoji');

-        expect(comp.getText()).toEqual('');
+        expect(comp.getText()).toEqual('', 'Should have empty text after deleting all emojis');
src/main/webapp/app/shared/monaco-editor/monaco-editor.component.ts (1)

160-196: Consider breaking down the complex backspace command logic.

While the implementation is correct, the code could be more maintainable if split into smaller, focused functions.

Consider refactoring into smaller functions:

+    private handleSelectionDelete(): void {
+        this._editor.trigger('keyboard', 'deleteLeft', null);
+    }
+
+    private getTextBeforeCursor(model: monaco.editor.ITextModel, lineNumber: number, column: number): string {
+        const lineContent = model.getLineContent(lineNumber);
+        return lineContent.substring(0, column - 1);
+    }
+
+    private deleteLastGrapheme(textBeforeCursor: string, textAfterCursor: string, lineNumber: number, lineContent: string): void {
+        const splitter = new GraphemeSplitter();
+        const graphemes = splitter.splitGraphemes(textBeforeCursor);
+        if (graphemes.length === 0) return;
+
+        graphemes.pop();
+        const newTextBeforeCursor = graphemes.join('');
+        const newLineContent = newTextBeforeCursor + textAfterCursor;
+
+        this.updateEditorContent(lineNumber, lineContent, newLineContent);
+    }
+
+    private updateEditorContent(lineNumber: number, oldContent: string, newContent: string): void {
+        this._editor.getModel()?.pushEditOperations(
+            [],
+            [{
+                range: new monaco.Range(lineNumber, 1, lineNumber, oldContent.length + 1),
+                text: newContent,
+            }],
+            () => null
+        );
+    }

     this.customBackspaceCommandId =
         this._editor.addCommand(monaco.KeyCode.Backspace, () => {
             const model = this._editor.getModel();
             const selection = this._editor.getSelection();
             if (!model || !selection) return;

             if (!selection.isEmpty()) {
-                this._editor.trigger('keyboard', 'deleteLeft', null);
+                this.handleSelectionDelete();
                 return;
             }

             const lineNumber = selection.startLineNumber;
             const column = selection.startColumn;
-            const lineContent = model.getLineContent(lineNumber);
-            const textBeforeCursor = lineContent.substring(0, column - 1);
-            const splitter = new GraphemeSplitter();
-            const graphemes = splitter.splitGraphemes(textBeforeCursor);
-
-            if (graphemes.length === 0) return;
-
-            graphemes.pop();
-            const newTextBeforeCursor = graphemes.join('');
             const textAfterCursor = lineContent.substring(column - 1);
+            const textBeforeCursor = this.getTextBeforeCursor(model, lineNumber, column);
+            const lineContent = model.getLineContent(lineNumber);
-            const newLineContent = newTextBeforeCursor + textAfterCursor;
-            model.pushEditOperations(
-                [],
-                [{
-                    range: new monaco.Range(lineNumber, 1, lineNumber, lineContent.length + 1),
-                    text: newLineContent,
-                }],
-                () => null
-            );
+            this.deleteLastGrapheme(textBeforeCursor, textAfterCursor, lineNumber, lineContent);
         }) || undefined;
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 833e46c and a072139.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • package.json (1 hunks)
  • src/main/webapp/app/shared/monaco-editor/model/actions/emoji.action.ts (1 hunks)
  • src/main/webapp/app/shared/monaco-editor/monaco-editor.component.ts (5 hunks)
  • src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts (1 hunks)
  • src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
src/main/webapp/app/shared/monaco-editor/model/actions/emoji.action.ts (1)

Pattern src/main/webapp/**/*.ts: angular_style:https://angular.io/guide/styleguide;methods_in_html:false;lazy_loading:true;code_reuse:true;tests:meaningful;types:PascalCase;enums:PascalCase;funcs:camelCase;props:camelCase;no_priv_prefix:true;strings:single_quotes;localize:true;btns:functionality;links:navigation;icons_text:newline;labels:associate;code_style:arrow_funcs,curly_braces,open_braces_same_line,indent_4;memory_leak_prevention:true;routes:naming_schema;chart_framework:ngx-charts;responsive_layout:true

src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts (1)

Pattern src/test/javascript/spec/**/*.ts: jest: true; mock: NgMocks; bad_practices: avoid_full_module_import; perf_improvements: mock_irrelevant_deps; service_testing: mock_http_for_logic; no_schema: avoid_NO_ERRORS_SCHEMA; expectation_specificity: true; solutions: {boolean: toBeTrue/False, reference: toBe, existence: toBeNull/NotNull, undefined: toBeUndefined, class_obj: toContainEntries/toEqual, spy_calls: {not_called: not.toHaveBeenCalled, once: toHaveBeenCalledOnce, with_value: toHaveBeenCalledWith|toHaveBeenCalledExactlyOnceWith}}

src/main/webapp/app/shared/monaco-editor/monaco-editor.component.ts (1)

Pattern src/main/webapp/**/*.ts: angular_style:https://angular.io/guide/styleguide;methods_in_html:false;lazy_loading:true;code_reuse:true;tests:meaningful;types:PascalCase;enums:PascalCase;funcs:camelCase;props:camelCase;no_priv_prefix:true;strings:single_quotes;localize:true;btns:functionality;links:navigation;icons_text:newline;labels:associate;code_style:arrow_funcs,curly_braces,open_braces_same_line,indent_4;memory_leak_prevention:true;routes:naming_schema;chart_framework:ngx-charts;responsive_layout:true

src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts (1)

Pattern src/test/javascript/spec/**/*.ts: jest: true; mock: NgMocks; bad_practices: avoid_full_module_import; perf_improvements: mock_irrelevant_deps; service_testing: mock_http_for_logic; no_schema: avoid_NO_ERRORS_SCHEMA; expectation_specificity: true; solutions: {boolean: toBeTrue/False, reference: toBe, existence: toBeNull/NotNull, undefined: toBeUndefined, class_obj: toContainEntries/toEqual, spy_calls: {not_called: not.toHaveBeenCalled, once: toHaveBeenCalledOnce, with_value: toHaveBeenCalledWith|toHaveBeenCalledExactlyOnceWith}}

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (8)
src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts (3)

331-347: LGTM! Well-structured test for combined emoji deletion.

The test effectively verifies that backspace correctly deletes an entire combined emoji (🇩🇪) without leaving partial characters.


378-393: LGTM! Comprehensive test for emoji deletion in mixed text.

The test effectively verifies that only the emoji is deleted while preserving surrounding text, which is a common real-world scenario.


395-408: LGTM! Precise cursor position verification after emoji deletion.

The test effectively verifies that the cursor is placed at the correct position (column 7) after deleting an emoji.

src/main/webapp/app/shared/monaco-editor/monaco-editor.component.ts (2)

13-13: LGTM! Added GraphemeSplitter for proper emoji handling.

The GraphemeSplitter library is essential for correctly handling grapheme clusters, especially for combined emojis.


488-490: LGTM! Clean getter implementation for backspace command ID.

The getter properly encapsulates access to the custom backspace command ID with correct typing.

src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts (2)

609-619: LGTM! Thorough test for emoji insertion.

The test effectively verifies emoji insertion, cursor positioning, and editor focus.


621-642: LGTM! Comprehensive test for emoji picker workflow.

The test effectively verifies the complete emoji selection flow, from picker interaction to insertion and cleanup.

package.json (1)

56-56: Grapheme-splitter Dependency Added

The new dependency "grapheme-splitter": "^1.0.4" has been introduced to support improved deletion of combined emojis by enabling grapheme-aware text manipulation. This addition directly addresses the PR objective of ensuring that complete emoji clusters (such as 🇩🇪) are correctly removed without leaving residual characters.

A couple of points to consider:

  • Version Verification: Please verify that version ^1.0.4 fully meets the grapheme-splitting requirements and that there are no known breaking changes or security issues with this version.
  • Integration Testing: Ensure that the integration tests covering the Monaco editor's backspace functionality adequately validate this enhancement.

Copy link

@ItsaaaMeMario ItsaaaMeMario left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested on TS3, when testing with combined emojis (like ©️,1️⃣,‼️), I get yellow rectangles around the emojis. But with flags, I don't get the yellow rectangles.
Bildschirmfoto 2025-02-01 um 16 50 40

@asliayk
Copy link
Contributor Author

asliayk commented Feb 1, 2025

Tested on TS3, when testing with combined emojis (like ©️,1️⃣,‼️), I get yellow rectangles around the emojis. But with flags, I don't get the yellow rectangles. Bildschirmfoto 2025-02-01 um 16 50 40

This issue is not related to this PR—you can see the yellow rectangle on other test servers as well. This PR only includes the deletion of emojis, along with a fix for the issue where emojis were only partially deleted during the deletion process.

Here is what I face on TS5 (not deployed in this PR)

image

@asliayk asliayk requested a review from ItsaaaMeMario February 1, 2025 16:05
Copy link

@ItsaaaMeMario ItsaaaMeMario left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works as described. The full emoji is deleted.

Copy link

@SimonKaran13 SimonKaran13 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested on TS3, works as expected.

PS: I noticed this while testing #10248. Maybe since it's similar to this PR you might be interested.

Copy link

@Feras797 Feras797 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tested on TS3, works as described

Copy link

@zagemello zagemello left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested on TS4, works as expected.

Copy link

@alekspetrov9e alekspetrov9e left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested on TS4, works as expected.

Copy link

github-actions bot commented Feb 3, 2025

⚠️ Unable to deploy to test servers ⚠️

Testserver "artemis-test4.artemis.cit.tum.de" is already in use by PR #10232.

@github-actions github-actions bot added the deployment-error Added by deployment workflows if an error occured label Feb 3, 2025
@SindiBuklaji SindiBuklaji added deploy:artemis-test2 and removed deployment-error Added by deployment workflows if an error occured labels Feb 3, 2025
Copy link

@SindiBuklaji SindiBuklaji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested on TS2. No partial characters were left after deleting👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bugfix client Pull requests that update TypeScript code. (Added Automatically!) component:Communication ready for review tests
Projects
Status: Ready For Review
Status: Todo
Development

Successfully merging this pull request may close these issues.

Communication: Deleting combined emojis not working properly
10 participants