To participate in this lab you will need to have one of the following IDEs setup and ready for use:
Additionally, your chosen IDE should have the necessary Copilot extension installed and authenticated to work with your GitHub account.
The primary example in this repository for this lab is written in C#. As such, you will need to install and be able to run the ASP.NET 9.0 Framework.
Acquiring the code can be accomplished via GitHub as a compressed archive or using git
.
In order to work from the same source code as in this lab, you will need to either have this repository checked out from GitHub or have downloaded the Archive file for the appropriate stage of the lab.
- 45 minutes
In this lab we are going to talk about how to use Copilot to get the most out of it. We are going to learn how to use Github Copilot for common development use cases. We are going to look at how to do these things and how to sidestep some of the challenges you might experience while using Copilot.
- Common problems and solutions
- Explaining code and errors
- Writing documentation
- Adding a feature to the codebase
While learning about doing these things we will talk about improving the output of Copilot both by applying the prompt crafting methods previously discussed and the best-practices when encountering issues like:
- Missing or changed elements of code
- Hallucinations
- Incomplete or naive implementations
- Stalling and repetitive answers
First, a little more theory. Based on the prompts you have crafted and fed into Copilot, or the amount of code you are working with, you may or may not get the results you wanted. There are a number of ways in which Copilot's answers can fall short of expectations. Commonly you may see it:
- stall on autocomplete
- repeat itself
- deviate from existing code in answer
- omit or hallucinate content
- provide an obsolete or dated answer
- give a naive or inefficient solution
- retract an answer that matches public code
- It can happen that Copilot does not autocomplete when it seems clear to the user that they are pausing for a suggestion. The simple fix here is to give it a little push by starting either a comment or a method and it should trigger copilot.
- It can also happen that Copilot will regenerate the same content over and over again as if it has forgotten that it just gave that code. This is a form of hallucination error and does illustrate why copilot is not in the driver's seat. It requires an experienced user to scrutinize the suggested code and decide if it should be incorporated into the codebase or if more context is required.
- As the developer you must carefully review the code provided by Copilot as the full example, if given, may not be an appropriate replacement for the code in place. Copilot may have chosen to change the implementation slightly from what is currently in place. This may be ok or you may wish to not allow your logic to drift away from its familiar form especially if the affected code is not part of the changes you are making. You can ask copilot to avoid changing the original code as much as possible or only show the changed code. It is, in any event, advisable to carefully review suggestions to make sure they agree with your codebase before replacing. Also, ensuring the releavant files are in scope of Copilot Chat when the prompt is given can help keep Copilot on track.
-
Copilot might refactor variable names in its answer. It is not clear why it does this nor why it might omit statements that exist in your method body but, again, this outlines why user review of all suggestions are necessary. An inability to understand the answers given by Copilot are a recipe for a quagmire of code that does not work.
-
Copilot might also omit a large body of code that appears in the answer to simplify the answer and draw your attention to the changed code. In all cases you need to understand what you are accepting into your codebase from Copilot.
- By virtue of the fact that the model Copilot was trained on has a cutoff date, this means that some answers may be inaccessible to Copilot. In these cases, familiarity with the newer components and frameworks will be essential for the developer to provide that oversight.
- Copilot may answer a tricky question with a simple, effective, but inefficient answer. It often finds an answer that meets the objectives but if efficiency isn't a stated goal in the prompt, it may not prioritize that and the developer will need to re-request that the answer be optimized in the way they desire. The lesson is that you cannot assume Copilot will always choose the optimal solution-- its goal is to meet the prompt and if efficiency isn't specified it might settle on the simplest answer.
- One setting in your GitHub account or Organization is the toggle that Copilot will filter results that match public code. This means that if it generates an answer that happens to match a public code solution for the same thing it will retract the answer. This can be very frustrating and actually fairly common. The simple solution to this is to add to the prompt the phrase "Do not match public code." This informs Copilot that it should attempt to originate a new answer specifically avoiding a public code match. It really works.
Keep these practices in mind when submitting prompts to Copilot. You will see many of these issues.
-
Perhaps one of your first forays into using Copilot will be to use it to gain a basic understanding of a codebase or segment of code that is unclearly documented or otherwise unfamiliar to you. It just so happens that we have a project from which to start such an exploration. The first "command" we can use to focus the intent of our prompt is the
/explain
command. This informs Copilot that the desire is for a specific kind of response focused on a detailed explanation of the selection, file, or message. -
You can trigger an inline request for code explanation by pressing the hotkey that opens Copilot inline and issuing the command
/explain
along with additional prompt cues.Figure 1: Prompting to explain inline in VSCode
-
You can, of course, prompt from an inline location or using the Copilot Chat sidebar. This can be helpful if the program logic is not very clear or unnecessarily convoluted.
Figure 2: Prompting to explain a method inline in Rider
-
The
/explain
keyword can be used to help understand cryptic error messages returned by the runtime or the build process. In VSCode the Copilot Chat can be directed to access@terminal
with the keyword/explain
to review the error output.Figure 3: Requesting an explanation in VSCode to a build warning in the terminal.
-
In IDEs that do not support the
@terminal
agent, you can cut-and-paste error messages into the chat window to include as part of the prompt.Figure 4: Requesting an explanation in JetBrains Rider to a build warning.
-
In terms of documentation, Copilot can relieve a lot of tedium. Copilot can generate documentation for you and it can adopt your particular style of documentation given enough examples to work from. So write a few comments yourself and Copilot will resume in a consistent way. The special command for this activity is
/doc
. -
If starting without any existing examples, it can be helpful to refer to a standard style.
Figure 5: Starting documentation (VSCode)
-
Once you have an example, Copilot can pick up the hint. Your inline suggestions should align with other examples:
Figure 6: Inline Commenting Suggestions
-
Another trick to use when generating documentation is to ask Copilot to create Mermaid.js diagrams. In this way you can have quick diagramming of Entity-Relationships, Classes, Interactions, etc. (See Example Diagrams) You will need to have an extension in your IDE that supports Mermaid diagrams to view them.
Figure 7: A request for a mermaid-based entity-relationship diagram.
-
The following is an example of a Mermaid diagram that Copilot generated for the chessweb application:
erDiagram GameState { Board Board PieceColor CurrentTurn bool IsGameOver Position LastMoveFrom Position LastMoveTo string FEN } Board { Piece Squares[8x8] } Piece { PieceColor Color Position Position PieceType Type string ImagePath char Symbol } Pawn { char Symbol } Queen { char Symbol } Position { int Row int Column } GameState ||--|| Board: contains Board ||--o{ Piece: contains Piece ||--|| Position: has Pawn ||--|| Piece: inherits Queen ||--|| Piece: inherits
To begin this part of the lab, you need to get the codebase branch set to this lab portion:
git switch lab1-1-development-use-cases-add-fen-box
-
Our example application is called ChessWeb. It is a simple Web Application designed to have just enough complexity in its function to be an interesting target for Copilot work. It can be found in the repository under chessweb-cs (C#), chessweb-node (NodeJS), and chessweb-java (Java).
-
This sample application is far from a complete Chess-playing interface. In fact, all it does is to provide a view of a chessboard that allows turn-based moves to be entered with very basic piece rules applied. It serves as a basis for extension using Copilot. If you don't have knowledge of or interest in Chess, don't worry, no expertise is required to participate in this part.
-
Let's take a look at what we have:
-
We can start the application by entering the
chessweb-cs
directory and running it.cd chessweb-cs dotnet run
-
If successful we can now open a browser to the URI
http://localhost:5111
and see something like this:Figure 8: Application Startup View
-
In this view we can see a familiar chessboard with pieces set up for the beginning of a game. There is a button that can be used to reset the position. The board is labeled on the ranks and files: ranks are labeled 1-8 and files are labeled a-h. Finally, there is a turn indicator to the right of the board that will display which side and which colour is due to play.
- A reasonable feature to add is a display of the current board position in a standard text-based representation known as Forsyth-Edwrads Notation or FEN. This is a text encoding that can be used to rebuild a Chess position and represent the current game state in both a human and machine-readable fashion. You can read more about FEN here.
As a simple visual explanation of the encoding, see the below diagram which identifies several example chess ranks (rows) with the corresponding FEN portion. (Source: Chess.com)
The rules are simple:
- black pieces are represented by lowercase letters r,n,b,q,k,p
- white pieces are represented by uppercase letters R,N,B,Q,K,P
- a chess board has coordinates from a1-h8 with a1 being on the bottom right
- each rank is represented in FEN from top to bottom 8->1 and left to right (a-h)
- sequential empty squares are grouped and given a number for the size of the gap
- There are some additional facts about the board position given at the end of the FEN as:
- side to move next (w or b)
- castling rights remaining (kingside and queenside for each player)
- en-passant target square (if applicable)
- current half-move clock
- current move number
We won't bother with the addtional facts yet as they do not affect our ability to add the simple FEN feature.
There are a number of ways to begin this work. Here I present two paths:
- An Holistic Approach: Ask copilot to solve it all at once
- A Systematic Approach: Apply some knowledge of the application and target our changes
One way could be to ask Copilot to do all the heavy lifting for us. Sometimes this can work but the more complicated your codebase, the dicier this becomes. Such an holistic approach might look like this:
-
I will give Copilot a suitable prompt to begin this change:
Figure 9: VSCode FEN Prompt with workspace specifier
Note: The above image includes use of the
@workspace
context specifier which (at the time of this writing) is only supported in VSCode.To get Copilot to expand its context to include your workspace you should eliminate distracting open files from the context (by closing all open files in the editor) This will ensure Copilot will widen its context for the following prompt.
To clarify, if you have an file open in the editor when you issue the above prompt, Copilot will constrain its answer to that file. You can confirm this at the end of the output:
Figure 10: Limited context indication.
The answer from copilot will include changes to the in-context files only. Depending on the change you want to make and your familiarity with the codebase, you may wish to increase the context or constrain it in this way.
In our case, we will close all open files in editor windows and issue this prompt.
Let's extend this interface to display the current board position as Forsyth-Edwards Notation (FEN). This should take the form of a text input box below the chessboard with the current FEN string of the board position that gets updated as pieces are moved on the board.
And, again, if using VSCode, you can choose the
@workspace
specifier to increase the scope explicitly. In which case it doesn't matter if there are open files, it will expand its context. -
We can expect a reponse similar to this from our prompt:
Figure 11: A response from Copilot to our prompt
-
Of course the response from Copilot will come with code as well. Any code that comes out of this will need to be reviewed and integrated into your codebase.
-
The first suggestion should have been to add an element to the chessboard view to house the FEN string display. You can compare to this example and apply as needed.
Figure 12: Code to add to index.cshtml for FEN container
Note that the name of the CSS class for the FEN container may not have been in context (it's defined in webroot/css/chess.css) and not named correctly in the Copilot response. Just ensure it's named like the above and you're good to go.
If your answer does not specify where to put the element, put it on line 58 just after the div labeled "game-controls".
-
The second suggestion should be to update the Game State with the FEN string.
Figure 13: Update Game State step
Note that the above does not include the full code output from Copilot. You will have to integrate the suggestion into your code as needed.
-
The third suggestion should be to update the GameController.
Figure 14: Update Game Controller code step
-
The fourth suggestion should be to update the index.cshtml for the game view when pieces are moved
Figure 15: Update index.cshtml for piece move
- At this point, if you've been making the changes as suggested by Copilot, you may have noticed that there's a problem. The Game State change included a call to a method called
GenerateFEN
but the Board class does not have this method. Alternatively, Copilot generated the answer correctly and you have a step 5 that does include it. In my case, it didn't happen. This is part of the reconciliation of Copilot answers. My response is to prompt Copilot to complete its answer:
Your answer was missing the GenerateFEN method referenced in GameState. Can you fix this?
Note: These steps include an implementation of the GenerateFEN in the Board.cs class. While this is fine and does work, a better location for this method is in the GameState class itself. Copilot may have simply suggested this approach and if you applied such a change, go with it.
-
Now Copilot has a multistep answer for us:
Figure 16: The first step of the supplemental prompt
This change should include the missing
GenerateFEN
method to insert to the Board class. -
The second change we need to apply is to expose the CurrentTurn member property in the Board class.
Figure 17: Step to expose the CurrentTurn member in the Board class
-
The third and final change is to
Figure 18: Instructions to set CurrentTurn in Board class.
-
After applying these changes we can run the app and confirm the new feature has been added:
dotnet run
Figure 18: The UI now shows the FEN and is refreshed when board state changes.
This concludes the set of changes using the holistic approach. You're done!
Alternatively, we might choose to attack this problem more directly as we have some familiarity with the codebase already and have ideas of our own where changes should be made. We're not asking Copilot to think for us so much as speed up the development effort.
What we know about the chessweb-cs app:
- It has a Piece class with specific implementations for each chess piece
- It has a Board class to track the movement of pieces on the board
- It has a GameState class to track the state of each game
- A Controller class to interface between UI and backend
- An HTML and javascript view to display the game.
We inspect our GameState class because it seems to be the highest level object before the handoff to the UI. Logically, it should be responsible for the representation of the FEN because it has access to the Board data as well as the additional information about player turns that aren't stored in the Board itself.
We find there isn't any method presently that does this so we find a likely place to start and prompt Copilot.
Note: In the Various IDEs you can trigger inline suggestions and prompting with:
OS | IDE | Inline Suggest | Inline Prompt |
---|---|---|---|
Win | VSCode | Alt+\ | Ctrl+Enter |
Win | Rider | Alt+\ | Ctrl+Shift+I |
Win | Visual Studio | Alt+\ | Ctrl+Enter |
OSX | VSCode | Option+\ | Cmd+I |
OSX | Rider | Option+\ | Ctrl+Shift+I |
Figure 19: Prompting within the Inline Copilot Prompt Window
We can choose to forego this even and create a comment that will spark Copilot into action:
Figure 20: Prompting within a comment
We should scrutinize the answer and make sure the method generated is going to work with our existing code. One thing the watch for here is how the FEN character is determined. If we didn't specify that the Piece.symbol should be used, we might have Copilot trying to invent a method to get the FEN Char and no such method exists anywhere here.
It might be necessary to correct the code we got. If so, we can simply highlight the problem code and use our hotkey to bring up Inline prompter:
Figure 21: Correcting the symbol assignment
Once we have the GenerateFEN method we need to use it within the class. There are a couple of places where we need to update the class:
- In the constructor
- When pieces are moved
- When the board is reset
So we should add a call to update the FEN property in each of these cases.
Figure 22: adding the call to GenerateFEN()
When we're happy we can move along. We now have a GameState that updates itself when events affecting the board position are triggered.
The next change we need to make is to the GameController class. This conveys changes in the backend to the frontend. It also has a "Move" action will be called when the UI processes a piece move event. The important part of this component is that it returns the FEN string to the UI once the change has been made. We need to make sure it does this.
At the end of the IActionResult Move method we return a message and this needs to contain the FEN string.
Figure 23: Prompting to update the successful move return action
Finally, we need to update the UI itself. The Views/Game/index.cshtml holds the chessboard view and it a little more to get our hands around because it's a mix of HTML and javascript. We also don't want to hunt around for exactly where to put this new code in so we can select all the code and trigger the inline prompter with our hotkey.
Figure 24: Prompting to update the index.cshtml
This should result in some in place replacements that cover both the FEN box and the javascript action that affects it. Apply those changes, save, and run.
At this point you should have a running version of the chessweb application that updates its FEN string after every move.