One‑Click VS Code Image Upload Extension to Supercharge Your Workflow
This article presents a step‑by‑step guide to building a VS Code extension that streamlines image handling by automatically resolving paths, compressing files with Tinify, uploading them to OSS, and inserting the resulting URLs, dramatically reducing repetitive work and improving development efficiency.
1. How to Improve R&D Efficiency?
When workload stays the same, the most direct way to boost efficiency is to eliminate repetitive tasks. This guide focuses on the repetitive work of image usage.
Developers usually reference images either by placing them in the project and using a file path, or by uploading them to a company file‑management system and inserting the generated link. Optimizing images also requires a compression tool.
The traditional approach is cumbersome, so we propose a one‑click tool that automates all these steps.
2. Product Analysis and Design
How the Plugin Works
From a usage perspective
Right‑click menu → Upload image → Insert link at cursor.
Supported file types: .jpeg, .jpg, .png
Upload methods:
Upload without selecting any text.
Upload when cursor selects an existing URL.
Upload when cursor selects a relative path.
Upload when cursor selects an alias path.
What We Implemented Internally
Overall upload flow
How to obtain the absolute file path
Compress image
Upload stream to OSS
Write path into the editor
3. Hands‑On
Now that the requirements and technical plan are clear, let’s start.
Project Setup
Use the official Yeoman generator to create a TypeScript project.
npm install -g yo generator-code
yo codeDevelopment
npm run watch – compile in watch mode.
F5 – launch the extension host for debugging.
Project Overview
package.json fields:
name and publisher form the extension ID ( . ).
main points to the entry file.
activationEvents list events that trigger activation.
contributes defines commands and menus.
Additional VS Code configuration.
{
"name": "...",
"publisher": "...",
"main": "...",
"activationEvents": ["file-master-management.uploadFile"],
"contributes": {
"commands": [{"command": "file-master-management.uploadFile", "title": "上传图片"}],
"menus": {"editor/context": [{"when": "editorFocus", "command": "file-master-management.uploadFile", "group": "navigation"}]},
"configuration": {"title": "file-master-management", "properties": {}}
},
"icon": "icon.png"
}Activation occurs via the menu command “file‑master‑management.uploadFile”, which runs extension.ts.
export function activate(context: vscode.ExtensionContext) {
let uploadFileCommand = vscode.commands.registerTextEditorCommand(
'file-master-management.uploadFile',
uploadFileMain,
);
context.subscriptions.push(uploadFileCommand);
}Next we look at the implementation of the upload.
Code Implementation
Upload Entry Point
The main function obtains the absolute path, compresses the image, uploads it to OSS, and inserts the URL.
export const uploadFileMain = async () => {
try {
const { filePath, delOriginalPath } = await getFilePath();
const compressRet = await compress(filePath);
const url = await upload2OSS(compressRet.readStream, compressRet.cachePath || delOriginalPath);
await addUrl2Editor(url);
} catch (error) {
// handle error
}
};Get Absolute File Path
export const getFilePath = (): Promise<{filePath:string; delOriginalPath?:string}> => {
return new Promise(async (resolve, reject) => {
try {
const activeEditor = await getActiveEditor();
const document = activeEditor.document;
const selection = activeEditor.selection;
const text = document.getText(selection);
const extname = path.extname(text);
if (validUrl.isUri(text) && imgExtname.includes(extname)) {
return getFilePathFromRemote(text).then(filePath => {
resolve({ filePath, delOriginalPath: filePath });
});
}
const { start, end, active } = selection;
if (start.line === end.line && start.character === end.character) {
getUrlFromOpenDialog().then(filePath => resolve({ filePath })).catch(reject);
return;
}
const filePath = (await getFilePathFromAbs(activeEditor))[0];
resolve({ filePath });
} catch (error) {
getUrlFromOpenDialog().then(filePath => resolve({ filePath })).catch(reject);
}
});
};Convert URL to Local Path
const getFilePathFromRemote = (url:string): Promise<string> => {
return new Promise((resolve, reject) => {
download(url, path.join(__dirname)).then(() => {
resolve(path.join(__dirname, path.basename(url)));
}).catch(() => {});
});
};Select Image via Dialog
export const getUrlFromOpenDialog = (options?: OpenDialogOptions): Promise<string> => {
return new Promise((resolve, reject) => {
const defaultOptions: OpenDialogOptions = {
canSelectFiles: true,
canSelectMany: false,
title: '请选择上传文件',
openLabel: '确认上传',
filters: { Images: imgExtname },
};
vscode.window.showOpenDialog(Object.assign(defaultOptions, options)).then(uri => {
const filePath = get(uri || {}, '0.path');
if (!filePath) {
reject(makeErrorMsg({ message: '文件选择失败' }, '本地文件选择'));
}
resolve(filePath);
});
});
};Resolve Alias Paths
Parse tsconfig/jsconfig to map alias to absolute paths.
const getAliasPathMap = (resource:Uri): Promise<Mapping[]> => {
return new Promise(async resolve => {
let mappings: Mapping[] = [];
try {
const workFolder = vscode.workspace.getWorkspaceFolder(resource);
if (workFolder) {
const parsedFiles = await findTsConfigFiles(workFolder);
for (const parsedFile of parsedFiles) {
const baseUrl = parsedFile?.compilerOptions?.baseUrl || '.';
const paths = parsedFile?.compilerOptions?.paths || {};
for (const [key, values] of Object.entries(paths)) {
if (typeof values === 'string') {
mappings.push({ key, value: path.join(workFolder.uri.fsPath, baseUrl, values) });
} else if (Array.isArray(values)) {
values.forEach(value => {
mappings.push({ key, value: path.join(workFolder.uri.fsPath, baseUrl, value) });
});
}
}
}
}
} catch {}
resolve(mappings.map(({ key, value }) => ({ key, value: path.normalize(value.replace(/\*/g, '') })));
});
};Compress Image
Use Tinify (free 500 compressions per month).
export const tinifyCompress = (filePath:string): Promise<ICompressRet> => {
return new Promise((resolve, reject) => {
const tinifyKey = configuration.tinifyKey;
tinify.key = tinifyKey;
const basename = path.basename(filePath);
const cachePath = path.resolve(__dirname, basename);
tinify.fromFile(filePath).toFile(cachePath).then(() => {
const readStream: ReadStream = filePath2ReadStream(cachePath);
resolve({ readStream, cachePath });
}).catch(error => {});
});
};Insert URL into Editor
export const addUrl2Editor = (url:string) => {
return new Promise(async (resolve, reject) => {
try {
const activeEditor = await getActiveEditor();
const selection = activeEditor.selection;
const { start, end, active } = selection;
if (start.line === end.line && start.character === end.character) {
activeEditor.edit(editBuilder => {
editBuilder.insert(active, url);
resolve('');
});
} else {
activeEditor.edit(editBuilder => {
editBuilder.replace(selection, url);
resolve('');
});
}
} catch (error) {
reject(makeErrorMsg(error, '将URL插入到编辑器内'));
}
});
};Packaging
Because the project uses pnpm, package with vsce package --no-dependencies or vsce package --yarn. The resulting VSIX can be installed in VS Code.
After packaging, the extension is ready for use.
4. Summary
We parse image addresses, compress them, upload to OSS, and write the URL back to the editor, saving time and improving page performance when many images are used.
References
[1] Official Yeoman scaffold – https://code.visualstudio.com/api/get-started/your-first-extension
[2] activationEvents – https://code.visualstudio.com/api/references/activation-events
[3] contributes – https://code.visualstudio.com/api/references/contribution-points
[4] TinyPNG key – https://tinify.cn/developers
[5] Publishing extensions – https://code.visualstudio.com/api/working-with-extensions/publishing-extension
[6] VSCE pnpm support – https://github.com/microsoft/vscode-vsce/issues/421
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
