Code Completion & Generation
In G01 you saw one suggestion. In this module you'll understand why suggestions look the way they do — and use that understanding to steer them, building Recipe Box's storage layer from comments alone.
How Completion Actually Works
Think of a session musician who joins your band mid-song. They don't ask what to play — they listen to the bars you've already played (what's before the cursor), glance at the chord chart on the stand (your other open files), notice where the song is heading (what's after the cursor), and improvise the next phrase in your style. A musician who only heard the last two notes would play something generic; the more of your song they hear, the more the phrase sounds like yours. Code completion works identically: the model reads the code around your cursor and your related files, then predicts what a competent developer would write next in this exact project. That's why the same keystrokes produce different suggestions in different files.
On each pause in typing, the extension assembles a prompt contextThe bundle of text sent to the model: code before the cursor (prefix), code after it (suffix), the file path and language, and snippets from related open files. — the prefix (code before your cursor), the suffix (code after it), and relevant content from other open tabs — and asks a fast Gemini model to fill in the middleA model technique where the AI generates text that fits between a given beginning and end, rather than only continuing from the end. This is why suggestions respect the code below your cursor.. The result renders as ghost text. Nothing is inserted until you accept.
Two consequences worth memorizing: (1) Your file is the prompt. Names, comments, and nearby code steer the output more than anything else. (2) Open tabs matter. Keeping the relevant model/schema file open in another tab measurably improves suggestions in the file you're editing.
Developers who treat completion as random magic accept ~30% of suggestions and rage-quit the rest. Developers who know "my file is the prompt" rename a variable, add one clarifying comment, or open the schema file in a second tab — and acceptance quality jumps. Same tool, different mental model, very different week.
Comment-Driven Generation
Completion's big sibling: instead of finishing your line, the model writes a whole function from a comment that states your intent. The skill is writing comments that read like a mini-specification, not a vague wish.
✗ Vague comment → generic code
# save recipe def save_recipe(
No types, no storage choice, no failure behavior. The model must guess all three — and its guesses may not match your project.
✓ Spec comment → your code
# Insert a recipe dict (name, ingredients list, # minutes int) into the SQLite 'recipes' table. # Return the new row id. Raise StorageError on # any sqlite3 error, never expose raw exceptions. def save_recipe(
Data shape, storage, return value, and error policy are all stated — so they all appear in the generated code.
A reliable generation comment answers four questions: WHAT does the function do (one verb phrase), with WHICH inputs/outputs (names and types), using WHERE (the library/store/API it should touch), and WHAT-IF (the failure behavior). Write those four and the model rarely surprises you.
Controls & Shortcuts
You'll use these constantly — learn them now so the lab flows:
| Action | VS Code | JetBrains |
|---|---|---|
| Accept full suggestion | Tab | Tab |
| Accept next word only | Ctrl+→ | Alt+→ (varies by keymap) |
| Dismiss suggestion | Esc | Esc |
| Cycle alternatives | Alt+] / Alt+[ | hover popup arrows |
| Open Gemini chat | Ctrl+Alt+G (or click ✦) | Gemini tool window |
Partial accept is the power move. When a 12-line suggestion is 80% right, don't accept-then-delete: accept word-by-word (Ctrl+→) up to the point where it diverges from your intent, then type your correction — the model immediately re-suggests from your correction. This loop — accept, correct, re-suggest — is how experienced users keep their hands on the wheel.
Lab: Build Recipe Box's Storage Layer
Goal: a working SQLite storage module, storage.py, written ~90% by Gemini from your spec-comments — verified by you at every step. You'll need the recipe-box folder from G01 open in your IDE.
Set the stage (this is prompt engineering too)
Create storage.py and paste this header. It's not decoration — the docstring and imports tell the model the conventions for everything that follows:
"""SQLite storage layer for Recipe Box.
Conventions:
- One module-level DB_PATH, default "recipes.db".
- Every public function opens its own connection (no globals).
- All sqlite3 errors are wrapped in StorageError.
- Recipes are plain dicts: {"id": int, "name": str,
"ingredients": list[str], "minutes": int}.
"""
import json
import sqlite3
DB_PATH = "recipes.db"
class StorageError(Exception):
"""Raised when any database operation fails."""
Generate the schema function from a spec-comment
Below the class, type this comment and the def line, then pause for ghost text. Read the suggestion against the comment before accepting:
# Create the 'recipes' table if it doesn't exist:
# id INTEGER PRIMARY KEY AUTOINCREMENT,
# name TEXT NOT NULL, ingredients TEXT (JSON-encoded list),
# minutes INTEGER NOT NULL CHECK (minutes > 0).
# Wrap sqlite3.Error in StorageError.
def init_db() -> None:
ingredients without JSON-encoding, reject it and re-trigger — the docstring convention says lists are JSON in a TEXT column.A correct result looks like this (yours may differ in details — that's fine if the contract holds):
def init_db() -> None:
try:
with sqlite3.connect(DB_PATH) as conn:
conn.execute(
"""CREATE TABLE IF NOT EXISTS recipes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
ingredients TEXT NOT NULL,
minutes INTEGER NOT NULL CHECK (minutes > 0)
)"""
)
except sqlite3.Error as err:
raise StorageError(f"init_db failed: {err}") from err
Generate the write path
Same technique, two more functions. Type each comment block, review, accept or correct:
# Insert a recipe and return its new id.
# recipe has keys: name (str), ingredients (list[str]), minutes (int).
# JSON-encode ingredients. Wrap sqlite3.Error in StorageError.
def add_recipe(recipe: dict) -> int:
# Return all recipes as a list of dicts (decode ingredients
# from JSON). Order by name. Wrap sqlite3.Error in StorageError.
def list_recipes() -> list[dict]:
add_recipe use a parameterized query (? placeholders, never f-strings — that's SQL injection)? Does it return cursor.lastrowid? Does list_recipes run json.loads on the ingredients column? Is every sqlite3.Error wrapped?Verify end-to-end
Append a self-test and run the module:
if __name__ == "__main__":
init_db()
rid = add_recipe({
"name": "Masala Chai",
"ingredients": ["black tea", "milk", "cardamom", "ginger"],
"minutes": 10,
})
print(f"inserted id={rid}")
for r in list_recipes():
print(r["id"], r["name"], r["ingredients"], f"{r['minutes']} min")
try:
add_recipe({"name": "Broken", "ingredients": [], "minutes": -3})
except StorageError as err:
print(f"correctly rejected bad data: {err}")
Optional: do it in Node.js
Same technique, different language — create storage.mjs, paste a header docstring describing the same conventions (use better-sqlite3 or node:sqlite), and generate initDb/addRecipe/listRecipes from comments. Notice that your spec-comment skills transfer unchanged: the four questions (WHAT/WHICH/WHERE/WHAT-IF) are language-agnostic.
You built a real storage module by writing specifications instead of implementations — and you reviewed every line before it entered your codebase. The header docstring acted as standing instructions (a taste of what GEMINI.md does project-wide in G05), the spec-comments controlled each function, and the self-test caught anything that slipped through. This write→review→verify loop is the foundation for everything ahead.
When Completion Hurts
An honest tool guide tells you where the tool is weak. Completion is the wrong instrument when:
- You don't yet know what you want. Ghost text will happily commit you to the first plausible idea. Think first, or use chat (G03) to explore options.
- The code is security-critical. Auth, crypto, payment logic: write it deliberately, use completion only for boilerplate, and review like an adversary.
- The API is newer than the model's knowledge. Suggestions may use renamed or deprecated calls. Trust your compiler/tests over ghost text, and feed current docs via context files (G05).
- You're learning a concept. If the goal is for you to learn recursion, accepting a perfect recursive function teaches you nothing. Disable suggestions temporarily (click the Gemini status bar icon → pause) — the tool should serve the goal, not replace it.
Studies of AI-assisted teams consistently find the productivity win comes with a code-review tax: more code gets written, so more code must be reviewed. Teams that skip the review step ship subtle bugs at a higher rate than before they had AI. The discipline you practiced in this lab — checklist-review before accepting — is what keeps you in the winning column.
Knowledge Check
1. What does the extension send to the model when generating an inline completion?
2. Which spec-comment element was responsible for the CHECK constraint appearing in init_db?
3. A 12-line suggestion is right until line 8, then wrong. Best workflow?
4. In the lab's review checklist, why insist on parameterized queries (? placeholders)?
5. When is it smart to pause completions entirely?
Quiz Complete!
Ready for G03: Chat, Smart Commands & Actions →