GitHub Copilot and Amazon CodeWhisperer in Action

A previous post exploring the usefulness of generative AI for programming tested whether GitHub Copilot and Amazon CodeWhisperer adapt to the user’s coding patterns. And I wrote that the not-so-great results didn’t match my real-world experience.

And who would have thought: the next day provided examples with much better results. I guess it’s a home run for the digital buddies though: two additions to a JavaScript file which means they only have to gather context from one file.

1. The Context

The JavScript file has about 500 lines. It provides interactivity and animations for the floor plan of a fictitious university library – a demo using generative AI for programming. Even the floor plan image was created by AI: Microsoft Copilot (previously called Bing Chat).

Graphic effects and user interactions happen on a canvas element in the HTML page. The floor plan image is the canvas element’s CSS background.

Here’s a screenshot of the floor plan when the user clicked shelf 1 in room 3 (R3S1): the shelf got a yellow border with its name inside. If the user clicks outside a shelf but inside a room that will be marked instead. This will eventually be used for example to filter a list of books next to the image.

For debugging purposes the red dot shows where the user actually clicked. All marks disappear automatically after a second or on the next click.

The first extension task is to make a shelf”s individual compartments clickable. The second one is to mark a book’s slot with an animation.

2. Task 1: Make Book Compartments Clickable

To make a shelf”s individual compartments clickable the onCanvasMouseDown() event handler needs to be extended. This is the existing code where just rooms and shelves are clickable:

// ... preceeding code ... 

function onCanvasMouseDown(event) {
    const canvas = document.getElementById("floorPlanCanvas");
    const context = canvas.getContext("2d");
    const bcr = canvas.getBoundingClientRect();
    const x = Math.floor((event.clientX - bcr.left) / scaleX);
    const y = Math.floor((event.clientY - bcr.top) / scaleY);
    context.save();
    context.fillStyle = "red";
    context.fillRect(x, y, 10, 10);
    context.restore();
    const room = getRoomFromCoordinates(x, y);
    if (room) {
        const searchInput = document.getElementById("searchInput");
        const shelf = getShelfFromCoordinates(x, y);
        if (shelf) {
            markShelf(context, shelf);
            setTimeout(function () { clearFloorPlan(context) }, 1000);
            searchInput.value = shelf;
        }
        else {
            markRoom(context, room);
            setTimeout(function () { clearFloorPlan(context) }, 1000);
            searchInput.value = room;
        }
    }
} // onCanvasMouseDown()

// ... more code ...

The highlighted line 18 shows where the additions have to go: if a (yet to be implemented) getCompartmentNoFromCoordinates() function returns a number the (already existing) markCompartment() function needs to be called. Only otherwise the shelf-marking takes place.

2.1. GitHub Copilot’s Results for Task 1, First Try

With the prompt on line 18 GitHub Copilot added the highlighted code:

// ... preceeding code ... 

function onCanvasMouseDown(event) {
    const canvas = document.getElementById("floorPlanCanvas");
    const context = canvas.getContext("2d");
    const bcr = canvas.getBoundingClientRect();
    const x = Math.floor((event.clientX - bcr.left) / scaleX);
    const y = Math.floor((event.clientY - bcr.top) / scaleY);
    context.save();
    context.fillStyle = "red";
    context.fillRect(x, y, 10, 10);
    context.restore();
    const room = getRoomFromCoordinates(x, y);
    if (room) {
        const searchInput = document.getElementById("searchInput");
        const shelf = getShelfFromCoordinates(x, y);
        if (shelf) {
            // if compartment clicked, mark compartment
            const compartmentNo = getCompartmentCount(shelf) * (y - getShelfCoordinates(shelf)[1]) / getShelfCoordinates(shelf)[3];
            if (compartmentNo >= 1 && compartmentNo <= getCompartmentCount(shelf)) {
                markCompartment(context, shelf, Math.floor(compartmentNo));
                setTimeout(function () { clearFloorPlan(context) }, 1000);
                searchInput.value = shelf + "C" + Math.floor(compartmentNo);
            }
            else {
                markShelf(context, shelf);
                setTimeout(function () { clearFloorPlan(context) }, 1000);
                searchInput.value = shelf;
            }
            markShelf(context, shelf);
            setTimeout(function () { clearFloorPlan(context) }, 1000);
            searchInput.value = shelf;
        }
        else {
            markRoom(context, room);
            setTimeout(function () { clearFloorPlan(context) }, 1000);
            searchInput.value = room;
        }
    }
} // onCanvasMouseDown()

// ... more code ...

As you see, Copilot is a bit off:

  • it forgot to remove the old code for marking the shelf (lines 30 – 32)
  • if you fix that you’ll see that it marks the compartment on top of the clicked one
  • instead of following the obvious pattern to create a getCompartmentNoFromCoordinates() function it coded that logic in place

2.2. GitHub Copilot’s Results for Task 1, Second Try

After removing Copilot’s changes to onCanvasMouseDown() and typing just function get Copilot generated a correct getCompartmentNoFromCoordinates() function!

Then I added the same prompt to onCanvasMouseDown() as before and this is Copilot’s result:

// ... preceeding code ... 

function getCompartmentNoFromCoordinates(shelf, x, y) {
    const compartmentCount = getCompartmentCount(shelf);
    const shelfCoordinates = getShelfCoordinates(shelf);
    const compartmentHeight = shelfCoordinates[3] / compartmentCount;
    const compartmentNo = Math.floor((y - shelfCoordinates[1]) / compartmentHeight) + 1;
    if (compartmentNo < 1 || compartmentNo > compartmentCount) {
        console.warn("getCompartmentNoFromCoordinates(): invalid compartmentNo " + compartmentNo);
        return;
    }
    return compartmentNo;
} // getCompartmentNoFromCoordinates()

// ... more code ...

function onCanvasMouseDown(event) {
    const canvas = document.getElementById("floorPlanCanvas");
    const context = canvas.getContext("2d");
    const bcr = canvas.getBoundingClientRect();
    const x = Math.floor((event.clientX - bcr.left) / scaleX);
    const y = Math.floor((event.clientY - bcr.top) / scaleY);
    context.save();
    context.fillStyle = "red";
    context.fillRect(x, y, 10, 10);
    context.restore();
    const room = getRoomFromCoordinates(x, y);
    if (room) {
        const searchInput = document.getElementById("searchInput");
        const shelf = getShelfFromCoordinates(x, y);
        if (shelf) {
            // if compartment clicked, mark compartment
            const compartmentNo = getCompartmentNoFromCoordinates(shelf, x, y);
            if (compartmentNo) {
                markCompartment(context, shelf, compartmentNo);
                setTimeout(function () { clearFloorPlan(context) }, 1000);
                searchInput.value = shelf + "C" + compartmentNo;
                return;
            }
            markShelf(context, shelf);
            setTimeout(function () { clearFloorPlan(context) }, 1000);
            searchInput.value = shelf;
        }
        else {
            markRoom(context, room);
            setTimeout(function () { clearFloorPlan(context) }, 1000);
            searchInput.value = room;
        }
    }
} // onCanvasMouseDown()

// ... more code ...

While I’d prefer else logic instead of return logic that code works!

2.3. Amazon CodeWhisperer’s Results for Task 1

Amazon CodeWhisperer generated the same code in onCanvasMouseDown() as GitHub Copilot on its second try – it forgot the return though and didn’t generate the getCompartmentNoFromCoordinates() function which it called without the shelf parameter.

With the same hint of typing just function get it generated a getShelfCoordinates() function that already existed. It did get the getCompartmentNoFromCoordinates() implementation correct after I provided the name. And after adding the return and the shelf parameter I had working code!

2.4. Conclusion for Task 1

Both tools needed a little hand-holding to get to working code but I’m still impressed – this is a huge time saver. Which tool works best will depend on your context and preferences.

3. Task 2: Add an Animation

A book’s slot is currently marked with just a vertical line by markBookSlot() when the user selects a book in the list next to the floor plan. That’s easy to miss.

The task is to generate the markBookSlotAnimated() function from a markBookSlot() copy with the comment in line 20 added:

// ... preceeding code ... 

function markBookSlot(context, shelf, compartmentNo, slotNo) {
    const bookSlotCoordinates = getBookSlotCoordinates(shelf, compartmentNo, slotNo);
    context.save();
    context.lineWidth = 3;
    context.strokeStyle = "yellow";
    context.strokeRect(
        bookSlotCoordinates[0],
        bookSlotCoordinates[1],
        bookSlotCoordinates[2],
        bookSlotCoordinates[3]
    );
    context.restore();
} // markBookSlot()

function markBookSlotAnimated(context, shelf, compartmentNo, slotNo) {
    const bookSlotCoordinates = getBookSlotCoordinates(shelf, compartmentNo, slotNo);
    context.save();
    // show animation with 10 rectangles getting smaller and converging on the book slot
    const numRectangles = 10;
    const rectangleOffset = 10;
    for (let i = 1; i <= numRectangles; i++) {
        setTimeout(function () {
            clearFloorPlan(context);
            context.lineWidth = 3;
            context.strokeStyle = "yellow";
            context.strokeRect(
                bookSlotCoordinates[0] - rectangleOffset * (numRectangles - i),
                bookSlotCoordinates[1] + rectangleOffset * (numRectangles - i),
                bookSlotCoordinates[2] + 2 * rectangleOffset * (numRectangles - i),
                bookSlotCoordinates[3] - 2 * rectangleOffset * (numRectangles - i)
            );
        }, 100 * i);
    }
    context.restore();
} // markBookSlotAnimated()

// ... more code ...

But while I pinky swear GitHub Copilot generated an initial version of that code neither it nor Amazon CodeWhisperer would do it now. They both missed the crucial setTimeout().

4. Conclusion

The key takeaway is the same as in the post that triggered this one: enjoy what you get “for free” when typing along and don’t try to force your digital pair programmer to code the solution you have in mind.

Here’s yet another example: after completing task 1 the compartments need a click-through border so users can still click the whole shelf. After I added the comment on line 8 GitHub Copilot came up with the solution on lines 9 – 12:

// ... preceeding code ... 

function getCompartmentNoFromCoordinates(x, y) {
    const shelf = getShelfFromCoordinates(x, y);
    if (shelf) {
        const compartmentCount = getCompartmentCount(shelf);
        const shelfCoordinates = getShelfCoordinates(shelf);
        // use a 4px click-through border for each compartment so users can still click the shelf
        if (x < shelfCoordinates[0] + 4 || x > shelfCoordinates[0] + shelfCoordinates[2] - 4 ||
            y < shelfCoordinates[1] + 4 || y > shelfCoordinates[1] + shelfCoordinates[3] - 4) {
            return;
        }
        const compartmentHeight = shelfCoordinates[3] / compartmentCount;
        const compartmentNo = Math.floor((y - shelfCoordinates[1]) / compartmentHeight) + 1;
        return compartmentNo;
    }
} // getCompartmentNoFromCoordinates()

// ... more code ...

Note that I wanted a click-through area around each compartment – no idea how I would implement that myself considering the current logic and data. Copilot instead created it around the whole block of a shelf’s compartments! That’s a much easier solution and good enough.

When AI just kicks in as you type along it can easily save 80 percent of a task’s coding time. Just don’t make the mistake of wasting that time trying to force solutions like in task 2. Accept that your digital buddy has its moods and limitations.

A possible problem is that you adapt to the AI’s style instead like using return instead of else logic (see section 2.2.). Currently your discipline is the only cure – like with so many things in life. Maybe the future will bring a means to override everybody’s bad habits – that’s where the AI got them 😉 – with a style guide or a company’s private code base.

And obviously AI works better in a “flat” context like JavaScript compared to more complex environments like ASP.NET Razor pages or Entity Framework – especially when everything happens in a single file.

Additional Resources

This entry was posted in Uncategorized and tagged , , . Bookmark the permalink.

Leave a comment