Skip to content

How I built the VSCode Extension I was missing

Published: at 08:04 PM

image.png

Table of Contents

Open Table of Contents

Introduction

When I first started this blog, I knew I didn’t want to store images locally in the GitHub repo I use as the source of truth. I wanted the images hosted externally on Cloudflare.

Until now, I had been uploading my images into a Cloudflare R2 bucket out of convenience. I found a VSCode extension that made it easy to upload images directly to an S3-compatible endpoint (like R2) and insert them into my markdown files. I use Astro for my blog which means that all of my blog posts have a markdown file as their source of truth.

That means that my images could be addressed by a custom R2 domain, such as https://r2.mdias.info/images/markdown-images/<image-file>.JPEG.

This approach worked well, but if I wanted to transform an image (like cropping it or adding a watermark), I’d have to use Cloudflare image transformations. While those work perfectly well, I’d have to bake the transformations into the image URL, which negates the convenience of having a fast upload workflow.

Using Cloudflare Images

Before proceeding, let me make the obvious disclaimer: I’m a proud Cloudflare employee. I’m naturally biased toward Cloudflare products and very familiar with them. With that said, I’d still recommend Cloudflare Images as a very performant and powerful image hosting service.

Cloudflare Images is Cloudflare’s native image hosting service. One of the best things about it is that you can apply transformations to your images by using variants.

Variants can be applied to images by simply appending a suffix to the Cloudflare Images URL with the format https://imagedelivery.net/<account_hash>/<image_id>/<variant_name>.

This is a much simpler way to apply transformations on the fly, which made me think: a VSCode extension could easily auto-upload an image to Cloudflare and auto-append a variant when inserting it into a markdown file.

image.png

For instance, the screenshot above is an image hosted on Cloudflare and it can be found at https://imagedelivery.net/LDaKen7vOKX42km4kZ-43A/a09dda38-9ab7-402e-cb9b-b9e857e77800/blog.

The starting point

One of the game-changing things about vibecoding is that it enabled a lot of technically-minded people to become “lightweight” software architects. If you know where the pieces fit, you can write comprehensive prompts describing the architecture of a piece of software and let a powerful LLM generate an initial project structure for it. It’s particularly useful for very specific use cases, like building a VSCode extension. So that’s exactly what I did.

The IDE I use for all of my projects is Windsurf. My LLM of choice is Claude Sonnet 4.5. Claude and I have become great friends over this past year. First-name basis.

Here’s the initial prompt I used to start the project:

Build a VSCode extension that:
- Uploads an image to Cloudflare Images and appends a variant to it when inserting it in a markdown file. A user should be able to drag and drop or paste an image straight into the editor and have it uploaded to Cloudflare Images and inserted into the markdown file with the variant appended to the URL. 
- The only settings for this extension should be the Cloudflare account hash, a Cloudflare API token with Image Upload permissions, and the variant name to be appended to the URL. 
- This extension should be aware of the format of the file currently open in the editor. 
- It should have a similar UX to the "Paste and Upload" extension by duanyll (https://marketplace.visualstudio.com/items?itemName=duanyll.paste-and-upload).

That was enough to build out the initial project structure. VSCode extensions follow a specific structure:

├── .vscode
│   ├── launch.json     // Config for launching and debugging the extension
│   └── tasks.json      // Config for build task that compiles TypeScript
├── .gitignore          // Ignore build output and node_modules
├── README.md           // Readable description of your extension's functionality
├── src
│   └── extension.ts    // Extension source code
├── package.json        // Extension manifest
├── tsconfig.json       // TypeScript configuration

And just like that, a project was generated for my extension:

image.png

Features

These features are all explained in the GitHub repository’s README.md file, but that too was partially written by the LLM.

I’ll use this post to explain the reasoning behind some of the features I decided to build into the extension.

The basics

The core of this extension is making it seamless to upload an image to Cloudflare Images and insert it into a text file.

I wanted the general UX to be very similar to duanyll’s “Paste and Upload” extension where an image can either be pasted or dragged and dropped into the editor.

Drag and Drop: DragandDrop_Export_small_gif.gif

Copy and Paste: CopyandPaste_Export_smlaller_gif.gif

I initially developed it with markdown files in mind but then expanded the functionality to more text formats. It is aware of the format of the open file and will insert the image to it with the correct format:

Markdown (.md):

![image-name.png](https://imagedelivery.net/YOUR_HASH/IMAGE_ID/public)

HTML (.html):

<img src="https://imagedelivery.net/YOUR_HASH/IMAGE_ID/public" alt="image-name.png" />

CSS (.css):

url('https://imagedelivery.net/YOUR_HASH/IMAGE_ID/public')

JavaScript/TypeScript:

"https://imagedelivery.net/YOUR_HASH/IMAGE_ID/public"

I also added an upload indicator notification so that the user is aware when an upload is in progress. This is useful because larger images can take longer to upload and without this indicator, the user might think that nothing is happening.

image.png

Duplicate detection

Something I missed from the original ‘Paste and Upload’ extension was the ability to detect if an image had already been uploaded to the bucket.

This reduces storage usage which is important as Cloudflare Images is a consumption-based product:

image.png

(from Cloudflare Images Pricing)

I decided to make this extension aware of whether an image had already been uploaded to Cloudflare Images by storing:

in VSCode’s Global State for 30 days.

let imageCache: ImageCache = {};
let globalState: vscode.Memento | undefined;

// Load from persistent storage
function loadImageCache(): void {
    if (globalState) {
        imageCache = globalState.get<ImageCache>('imageCache', {});
    }
}

// Save to persistent storage
async function saveImageCache(): Promise<void> {
    if (globalState) {
        await globalState.update('imageCache', imageCache);
    }
}

I had to use local persistent storage as Cloudflare’s API does not publish the md5 hash of uploaded images, which we could otherwise have used to check if an image had already been uploaded. This also makes it both extremely fast at detecting duplicates, and makes it so the info is persistent across IDE restarts.

DuplicateDetection_Export_smaller_gif.gif

When the extension detects that the image pasted or dropped into the file has already been uploaded, it will reuse the URL that was generated when the image was first uploaded, reducing storage usage.

The extension will also notify the user of this via in-IDE notifications, triggered via the vscode.window.showInformationMessage() API endpoint:

image.png

Auto-delete

I am particularly proud of this one.

It’s easy to inadvertently upload an image to Cloudflare. All it takes is forgetting that you used your clipboard manager to backtrack through clipboard history and pasting an unintended image, or accidentally dragging an image file to the editor.

The VSCode API does not expose the undo operation (⌘/Ctrl + Z), so we can’t use it to trigger an action. This means I had to get a bit more creative.

When an image is uploaded, the extension stores the following data in RAM (given the use-case, storing it in persistent storage would not work):

function trackInsertedImage(url: string, documentUri: string): void {
    const imageId = extractImageIdFromUrl(url);
    if (!imageId) {
        return;
    }
    
    recentlyInsertedImages.set(url, {
        imageId,
        url,
        documentUri,
        insertedAt: Date.now()
    });
    
    // Auto-cleanup after tracking duration
    setTimeout(() => {
        recentlyInsertedImages.delete(url);
    }, TRACKING_DURATION);
}

It uses this to track the “existence” of an image in the file. It listens to every text change in your documents.

When text is deleted, it:

vscode.workspace.onDidChangeTextDocument(async (event) => {
    // Checks if deleteOnRemoval is enabled
    const deleteOnRemoval = config.get<boolean>('deleteOnRemoval', false);
    
    // Monitors text changes in the document
    for (const change of event.contentChanges) {
        if (change.text === '' && change.rangeLength > 0) {
            // Text was deleted!
        }
    }
});

After testing locally, I realized that having the extension automatically delete an image wasn’t always a good idea. I assumed an accidental upload wouldn’t happen too often, so the extension instead asks the user if they want to proceed with deleting the image from Cloudflare or not. I used the vscode.window.showWarningMessage() API endpoint:

image.png

If ‘Delete’ is pressed, then the extension will send an API call to Cloudflare to delete the image.

async function deleteImageFromCloudflare(imageId: string, config: CloudflareConfig) {
    const response = await fetch(
        `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1/${imageId}`,
        { method: 'DELETE', headers: { 'Authorization': `Bearer ${apiToken}` } }
    );
}

If the image was successfully deleted, it will trigger a success message:

image.png

All of this to say: even though there’s no VSCode API to track the undo operation, this functionality makes it feel like there is. When you undo pasting an image, the extension interprets that as the URL being deleted from the document, triggering the deletion flow!

This functionality is a toggleable setting in the extension settings and is disabled by default.

Injecting metadata

Cloudflare Images now allows you to inject metadata when uploading an image. This can be very useful if you use Cloudflare to store images for a lot of different applications and want to tag specific images with some sort of identifier, for example, identifying images that were uploaded to Cloudflare via this extension.

I added this functionality on version 0.4.0 of the extension.

Metadata can be inserted as part of the payload of the API call to the image upload endpoint of Cloudflare’s API https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/images/v1:

curl --request POST \
https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1 \
--header "Authorization: Bearer <API_TOKEN>" \
--form 'url=https://[user:password@]example.com/<PATH_TO_IMAGE>' \
--form 'metadata={"key":"value"}' \
--form 'requireSignedURLs=false'

(from Cloudflare Developer Docs)

The extension injects the following parameters as metadata:

if (addMetadata) {
            const metadata = {
                uploadedBy: 'vscode-cloudflare-images-extension',
                version: '0.4.0',
                uploadedAt: new Date().toISOString(),
                fileName: fileName || path.basename(imagePath)
            };
            formData.append('metadata', JSON.stringify(metadata));
        }

Like the auto-delete feature, this is also a toggleable setting in the extension settings. Unlike the auto-delete feature, this one is enabled by default.

Here’s what it looks like when an image is uploaded with metadata:

image.png

In summary, the only settings required to use this extension are:

image.png

Publishing the extension

image.png

I used vsce and ovsx tools to package and publish the extension.

I published this extension on both the Visual Studio Marketplace (which is the default marketplace for VSCode and popular forks like Cursor) and the Open VSX Registry (which is the default marketplace for other popular VSCode forks like Windsurf and Trae).

This makes it so that the extension can be easily found by searching for it on the marketplace:

image.png

I made an admittedly horrible-looking logo in Photoshop, but I did include one of my own drone photos in it as an easter egg:

image.png

Yeah, I might revisit the logo situation in the future…

Typical use

One of my favorite ways to use this extension is when writing these blog posts. I use screenshots very frequently, and this extension makes it a breeze to insert new screenshots into my posts.

ScreenshotWorkflow.gif

GitHub repo

This extension is fully open-source and available on GitHub at mcdays94/cloudflare-images-upload-extension.

Feel free to contribute!

All images in this blog post were inserted using this extension. If you are looking for a quick and easy way to upload and insert images into your text files, give it a try!


Next Post
A Geek's Approach to Guest WiFi: From NFC Tags to Secure DNS