|
|
@@ -0,0 +1,176 @@
|
|
|
+import * as vscode from 'vscode';
|
|
|
+
|
|
|
+/**
|
|
|
+ * Infer language identifier from file extension.
|
|
|
+ */
|
|
|
+function getLanguage(filePath: string): string {
|
|
|
+ const ext = filePath.split('.').pop() ?? '';
|
|
|
+ const langMap: Record<string, string> = {
|
|
|
+ ts: 'typescript',
|
|
|
+ tsx: 'typescriptreact',
|
|
|
+ js: 'javascript',
|
|
|
+ jsx: 'javascriptreact',
|
|
|
+ json: 'json',
|
|
|
+ md: 'markdown',
|
|
|
+ css: 'css',
|
|
|
+ html: 'html',
|
|
|
+ py: 'python',
|
|
|
+ rs: 'rust',
|
|
|
+ go: 'go',
|
|
|
+ java: 'java',
|
|
|
+ rb: 'ruby',
|
|
|
+ php: 'php',
|
|
|
+ yml: 'yaml',
|
|
|
+ yaml: 'yaml',
|
|
|
+ sh: 'bash',
|
|
|
+ bash: 'bash',
|
|
|
+ sql: 'sql',
|
|
|
+ vue: 'vue',
|
|
|
+ svelte: 'svelte',
|
|
|
+ };
|
|
|
+ return langMap[ext] ?? ext;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Format a single file's content as AI context.
|
|
|
+ * Output: [path/to/file.ts]\n```lang\ncontent\n```\n
|
|
|
+ */
|
|
|
+export function formatFileContent(filePath: string, content: string): string {
|
|
|
+ const lang = getLanguage(filePath);
|
|
|
+ return [
|
|
|
+ `[${filePath}]`,
|
|
|
+ '```' + lang,
|
|
|
+ content,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ ].join('\n');
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Format a selection within a file as AI context.
|
|
|
+ * Output: [path/to/file.ts LnCn-LnCn]\n```lang\ncontent\n```\n
|
|
|
+ */
|
|
|
+export function formatFileSelection(
|
|
|
+ filePath: string,
|
|
|
+ content: string,
|
|
|
+ startLine: number,
|
|
|
+ startCol: number,
|
|
|
+ endLine: number,
|
|
|
+ endCol: number,
|
|
|
+): string {
|
|
|
+ const lang = getLanguage(filePath);
|
|
|
+ const header = `${filePath} L${startLine}C${startCol}-L${endLine}C${endCol}`;
|
|
|
+ return [
|
|
|
+ `[${header}]`,
|
|
|
+ '```' + lang,
|
|
|
+ content,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ ].join('\n');
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Format multiple file contents, joining with blank lines.
|
|
|
+ */
|
|
|
+export function formatMultiFile(entries: { filePath: string; content: string }[]): string {
|
|
|
+ return entries.map(e => formatFileContent(e.filePath, e.content)).join('\n');
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Copy the current editor selection to clipboard with AI-friendly formatting.
|
|
|
+ */
|
|
|
+export async function copySelection(): Promise<void> {
|
|
|
+ const editor = vscode.window.activeTextEditor;
|
|
|
+ if (!editor) return;
|
|
|
+
|
|
|
+ const selection = editor.selection;
|
|
|
+ if (selection.isEmpty) return;
|
|
|
+
|
|
|
+ const document = editor.document;
|
|
|
+ const filePath = vscode.workspace.asRelativePath(document.uri);
|
|
|
+ const text = document.getText(selection);
|
|
|
+
|
|
|
+ // VS Code selections are 0-indexed for lines, columns are 0-indexed UTF-16 code units
|
|
|
+ const startLine = selection.start.line + 1;
|
|
|
+ const startCol = selection.start.character + 1;
|
|
|
+ const endLine = selection.end.line + 1;
|
|
|
+ const endCol = selection.end.character + 1;
|
|
|
+
|
|
|
+ const result = formatFileSelection(filePath, text, startLine, startCol, endLine, endCol);
|
|
|
+
|
|
|
+ await vscode.env.clipboard.writeText(result);
|
|
|
+ showStatus(`$(clippy) Copied ${filePath}:${startLine}-${endLine}`);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Copy the entire active file to clipboard with AI-friendly formatting.
|
|
|
+ */
|
|
|
+export async function copyActiveFile(): Promise<void> {
|
|
|
+ const editor = vscode.window.activeTextEditor;
|
|
|
+ if (!editor) return;
|
|
|
+
|
|
|
+ const document = editor.document;
|
|
|
+ const filePath = vscode.workspace.asRelativePath(document.uri);
|
|
|
+ const content = document.getText();
|
|
|
+ const result = formatFileContent(filePath, content);
|
|
|
+
|
|
|
+ await vscode.env.clipboard.writeText(result);
|
|
|
+ showStatus(`$(clippy) Copied file: ${filePath}`);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Select files from the workspace and copy their contents.
|
|
|
+ */
|
|
|
+export async function copyWorkspaceFiles(): Promise<void> {
|
|
|
+ const workspaceFolders = vscode.workspace.workspaceFolders;
|
|
|
+ if (!workspaceFolders || workspaceFolders.length === 0) return;
|
|
|
+
|
|
|
+ const uris = await vscode.window.showOpenDialog({
|
|
|
+ canSelectFiles: true,
|
|
|
+ canSelectFolders: false,
|
|
|
+ canSelectMany: true,
|
|
|
+ openLabel: 'Select files to copy as context',
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!uris || uris.length === 0) return;
|
|
|
+
|
|
|
+ await copyFilesFromUris(uris);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Copy files selected in the explorer context menu.
|
|
|
+ */
|
|
|
+export async function copySelectedFiles(selectedUris: vscode.Uri[]): Promise<void> {
|
|
|
+ if (!selectedUris || selectedUris.length === 0) return;
|
|
|
+ await copyFilesFromUris(selectedUris);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Read and format multiple files, then copy to clipboard.
|
|
|
+ */
|
|
|
+async function copyFilesFromUris(uris: vscode.Uri[]): Promise<void> {
|
|
|
+ const entries: { filePath: string; content: string }[] = [];
|
|
|
+
|
|
|
+ for (const uri of uris) {
|
|
|
+ try {
|
|
|
+ const doc = await vscode.workspace.openTextDocument(uri);
|
|
|
+ const filePath = vscode.workspace.asRelativePath(uri);
|
|
|
+ entries.push({ filePath, content: doc.getText() });
|
|
|
+ } catch {
|
|
|
+ // skip binary or unreadable files
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (entries.length === 0) return;
|
|
|
+
|
|
|
+ const result = formatMultiFile(entries);
|
|
|
+ await vscode.env.clipboard.writeText(result);
|
|
|
+ showStatus(`$(clippy) Copied ${entries.length} file(s)`);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Show a temporary status bar message.
|
|
|
+ */
|
|
|
+function showStatus(message: string): void {
|
|
|
+ vscode.window.setStatusBarMessage(message, 3000);
|
|
|
+}
|