olivierdagenais / tampermonkey-copy-url

A Tampermonkey userscript to copy nice-looking URLs to the clipboard.
MIT License
0 stars 0 forks source link

Bitbucket DC's pull request "smart" text editor doesn't handle HOME/END keypresses as expected #51

Closed olivierdagenais closed 3 months ago

olivierdagenais commented 5 months ago

For a long line which is soft-wrapped across a few lines, hitting HOME or END goes to the line's absolute beginning or end, rather than that of the current soft-wrapped (virtual) line (which all the other editors in the world do).

Last observed in version 8.9.9, logged with Atlassian on 2020/11/10 as BSERV-12651: Home key has unexpected behavior in comment editor

olivierdagenais commented 4 months ago

codemirror.js, lines 571-573 reads:

  // The DOM events that CodeMirror handles can be overridden by
  // registering a (non-DOM) handler on the editor for the event name,
  // and preventDefault-ing the event in that handler.

It's possible we're dealing with a call to moveToLineBoundary where the includeWrap parameter is misconfigured.

olivierdagenais commented 4 months ago

Bitbucket 8.9.9 ships with CodeMirror 5.60.0. Using the following test.html content, I was able to fix the editor by using the extraKeys option that replaces the Home & End keys with more appropriate commands:

<html>
    <head>
        <script src="lib/codemirror.js"></script>
        <link rel="stylesheet" href="lib/codemirror.css">
        <script src="mode/javascript/javascript.js"></script>
    </head>
    <body>
        <textarea id="code">
        @codemirror and @lezer packages can be imported directly by name. Other imports should be URLs. You'll usually want your script to create an editor in document.body. Press Ctrl-Space to run the current code, and see the result (boxed in an <iframe>) in the “Ouput” tab. When errors occur or things are logged to the console, you can find them in the “Log” tab.
        </textarea>
        <script>
        const codeMirror = CodeMirror.fromTextArea(document.getElementById("code"), {
            lineNumbers: true,
            lineWrapping: true,
            mode: "htmlmixed"
        });
        codeMirror.setOption("extraKeys", {
            "End": "goLineRight",
            "Home": "goLineLeftSmart"
        });
        </script>
    </body>
</html>

Next up is figuring out how to set these options in Bitbucket's CodeMirror instance.

olivierdagenais commented 4 months ago

In theory, this works:

const codeMirrorWrappedDiv: CodeMirrorWrappedDiv | null =
    bitbucketBody.querySelector("div.CodeMirror.CodeMirror-wrap");
if (codeMirrorWrappedDiv) {
    const editor = codeMirrorWrappedDiv.CodeMirror;
    if (editor) {
        var extraKeys = editor.getOption("extraKeys");
        if (!extraKeys || typeof extraKeys === "string") {
            extraKeys = {};
            editor.setOption("extraKeys", extraKeys);
        }
        extraKeys.Home = "goLineLeftSmart";
        extraKeys.End = "goLineRight";
    }
}

...however, the current location of that code (page load) is no good because most of the time the CodeMirror-based editors are created dynamically, in response to activating a button.

olivierdagenais commented 4 months ago

The elements retrieved from the following selectors would be worth monitoring for the presence of div.CodeMirror.CodeMirror-wrap:

  1. div.comment-editor-wrapper
  2. div.atlaskit-portal-container
  3. div.comment-content

Additionally, a monitor could be added to div.activities to watch for new elements matching the selector div.activity-item.commented-activity.

Still missing: comments added to files/lines.

Perhaps, as a start, we attach an observer to div.tabs-pane.active-pane (because its contents changes drastically/entirely based on which tab is active) and see what the performance is like before trying to target a list of elements to observe. That still wouldn't cover the editor for the pull request description, since it lives under div.atlaskit-portal-container.

olivierdagenais commented 4 months ago

The observer technique worked for the Bitbucket code search enhancement, but when I tried it on the Bitbucket tab to enhance CodeMirror, it was firing on every letter typed, since that counts as a descendant mutation.

New idea: can we trap Home/End and then only enhance the nearest CodeMirror (if we find any!) before passing on the event? That way, we only spend time "enhancing" whenever those keys are used, which is far less than the time spent typing.

olivierdagenais commented 4 months ago

OK, this seems to work but not on the first press of Home or End:

async function handleKeydown2(this: Window, e: KeyboardEvent) {
    const document: Document = window.document;
    if (bitbucketBody && (e.key == "Home" || e.key == "End")) {
        const divs = bitbucketBody.querySelectorAll<CodeMirrorWrappedDiv>(
            "div.CodeMirror.CodeMirror-wrap"
        );
        for (let i = 0; i < divs.length; i++) {
            const div = divs[i];
            const editor = div.CodeMirror;
            if (editor) {
                var extraKeys = editor.getOption("extraKeys");
                if (!extraKeys || typeof extraKeys === "string") {
                    extraKeys = {};
                    editor.setOption("extraKeys", extraKeys);
                }
                extraKeys.Home = "goLineLeftSmart";
                extraKeys.End = "goLineRight";
            }
        }
    }
}

...this event was only registered if we detected the pull request tabs div.

Also, the version of CodeMirror used by Bitbucket might have a defect; the goLineRight makes the cursor go after the last space on the current virtual line, which means it ends up on the first column of the next line, yet behaves as if it was on the last column of the current line. UPDATE: this odd behaviour is the result of using inputStyle: "contenteditable" (as opposed to textarea) and is a known defect with version 5: codemirror/codemirror5#6274

yurikhan commented 4 months ago

Have you explored setting extraKeys on the global CodeMirror.defaults? Does that not work because Bitbucket sets its own extraKeys on instances and the defaults do not kick in?

olivierdagenais commented 4 months ago

Have you explored setting extraKeys on the global CodeMirror.defaults?

Thanks for the suggestion! Sometimes the easy thing is the best way, however in this scenario it doesn't seem to work:

  1. I tried doing this from my Tampermonkey script but Typescript insisted I reference the CodeMirror library before I would be able to reach the global CodeMirror object.
  2. I then created the simplest Tampermonkey script possible:

    // ==UserScript==
    // @name         Bitbucket CodeMirror
    // @namespace    http://tampermonkey.net/
    // @version      2024-03-02
    // @description  try to take over the world!
    // @author       You
    // @match        http://localhost:7990/bitbucket/*
    // @grant        none
    // ==/UserScript==
    
    (function() {
        'use strict';
    
        CodeMirror.defaults.extraKeys = {
            Home: "goLineLeftSmart",
            End: "goLineRight",
        };
    
    })();

    ...but, once an editor was created, I could see it wasn't working, likely because they had provided their own content for extraKeys and thus whatever is defined in CodeMirror.defaults isn't used. Here's what the CodeMirror instance had:

    
    extraKeys: Object {
        Enter: "newlineAndIndentContinueMarkdownList",
        Tab: false,
        "Shift-Tab": false
    }
    ​​​    ```