diff --git a/src/panels/helpers/contextHelper.ts b/src/panels/helpers/contextHelper.ts index 91e6ca2..dbe9100 100644 --- a/src/panels/helpers/contextHelper.ts +++ b/src/panels/helpers/contextHelper.ts @@ -102,3 +102,46 @@ export async function handleAddContextDocument(panel: vscode.WebviewPanel) { }); } } + +export async function handleAddContextDocumentSet(panel: vscode.WebviewPanel) { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + vscode.window.showWarningMessage("请先打开一个工作区"); + return; + } + + const files = await vscode.workspace.findFiles( + "**/*.{md,txt,pdf,doc,docx}", + "**/node_modules/**", + ); + + panel.webview.postMessage({ + command: "showWorkspaceDocumentSetList", + files: files.map((uri) => ({ + path: uri.fsPath, + relativePath: vscode.workspace.asRelativePath(uri), + })), + }); +} + +export async function handleGetDocumentSetList(panel: vscode.WebviewPanel) { + const documents = getDocumentSet(); + panel.webview.postMessage({ + command: "showDocumentSetList", + documents: documents, + }); +} + +let documentSet: Array<{ path: string; relativePath: string }> = []; + +export function getDocumentSet() { + return documentSet; +} + +export function addToDocumentSet(docs: Array<{ path: string; relativePath: string }>) { + documentSet = [...documentSet, ...docs]; +} + +export function saveDocumentSet(docs: Array<{ path: string; relativePath: string }>) { + documentSet = docs; +} diff --git a/src/panels/helpers/messageRouter.ts b/src/panels/helpers/messageRouter.ts index 2f78904..ecc9adf 100644 --- a/src/panels/helpers/messageRouter.ts +++ b/src/panels/helpers/messageRouter.ts @@ -32,6 +32,8 @@ import { handleAddContextFolder, handleAddContextImage, handleAddContextDocument, + handleAddContextDocumentSet, + handleGetDocumentSetList, } from "./contextHelper"; import { openFile, openFileWithSelection, openFilePathTag } from "./fileHelper"; @@ -356,6 +358,90 @@ export async function handleWebviewMessage( await handleAddContextFolder(panel); break; + case "addContextDocumentSet": + await handleAddContextDocumentSet(panel); + break; + + case "getDocumentSetList": + await handleGetDocumentSetList(panel); + break; + + case "saveDocumentSet": + const { saveDocumentSet } = await import("./contextHelper"); + saveDocumentSet(message.documents || []); + break; + + case "openContextSettings": + panel.webview.postMessage({ + command: "openSettingsTab", + tab: "context" + }); + break; + + case "selectFilesForDocset": + const uris = await vscode.window.showOpenDialog({ + canSelectMany: true, + canSelectFiles: true, + canSelectFolders: false, + openLabel: "选择文件", + filters: { + "支持的文件": ["md", "txt", "v", "sv", "pdf"], + }, + }); + + if (uris && uris.length > 0) { + const fs = require("fs"); + const path = require("path"); + const selectedFiles: Array<{ path: string; relativePath: string; size: number }> = []; + const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB + const MAX_TOTAL_SIZE = 50 * 1024 * 1024; // 50 MB + const MAX_FILES = 1000; + let totalSize = 0; + const errors: string[] = []; + + for (const uri of uris) { + const filePath = uri.fsPath; + const ext = path.extname(filePath).toLowerCase(); + + if (![".md", ".txt", ".v", ".sv", ".pdf"].includes(ext)) { + errors.push(`文件 ${path.basename(filePath)} 格式不支持`); + continue; + } + + try { + const stat = fs.statSync(filePath); + if (stat.size > MAX_FILE_SIZE) { + errors.push(`文件 ${path.basename(filePath)} 超过 10 MB`); + continue; + } + if (totalSize + stat.size > MAX_TOTAL_SIZE) { + errors.push("文档集总大小超过 50 MB"); + break; + } + if (selectedFiles.length >= MAX_FILES) { + errors.push("文件数量超过 1000 个"); + break; + } + + selectedFiles.push({ + path: filePath, + relativePath: vscode.workspace.asRelativePath(filePath), + size: stat.size, + }); + totalSize += stat.size; + } catch (err) { + errors.push(`无法读取文件 ${path.basename(filePath)}`); + } + } + + panel.webview.postMessage({ + command: "filesSelectedForDocset", + files: selectedFiles, + errors: errors.length > 0 ? errors : undefined, + }); + } + break; + case "addContextImage": await handleAddContextImage(panel); break; diff --git a/src/views/contextButton.ts b/src/views/contextButton.ts index 3fae63c..a74aac1 100644 --- a/src/views/contextButton.ts +++ b/src/views/contextButton.ts @@ -41,18 +41,15 @@ export function getContextButtonContent(): string { - - + 文档集 + + + + @@ -382,6 +379,48 @@ export function getContextButtonScript(): string { vscode.postMessage({ command: 'addContextFolder' }); } + // 显示文档集列表 + function showDocumentSetList() { + vscode.postMessage({ command: 'getDocumentSetList' }); + } + + // 显示文档集视图 + function showDocumentSetView(documents) { + const mainMenu = document.getElementById('contextMenuMain'); + const listView = document.getElementById('contextMenuList'); + const titleEl = document.getElementById('contextMenuListTitle'); + const bodyEl = document.getElementById('contextMenuListBody'); + + if (mainMenu && listView && titleEl && bodyEl) { + mainMenu.style.display = 'none'; + listView.style.display = 'flex'; + titleEl.textContent = '文档集'; + + if (documents.length === 0) { + bodyEl.innerHTML = \` +
+

暂无文档

+ +
+ \`; + } else { + currentListType = 'documentSetItem'; + currentListData = documents; + filteredListData = documents; + selectedItems.clear(); + renderList(documents); + } + } + } + + // 添加文档到文档集 + function addDocumentToSet() { + vscode.postMessage({ command: 'openContextSettings' }); + toggleContextMenu(); + } + + // 添加文档集项到上下文(已删除,使用统一的确认选择) + // 返回主菜单 function backToMainMenu() { const mainMenu = document.getElementById('contextMenuMain'); @@ -478,9 +517,16 @@ export function getContextButtonScript(): string { const selected = currentListData.filter(item => selectedItems.has(item.path)); if (selected.length > 0) { - selected.forEach(item => { - addContextItem(currentListType, item.path, item.relativePath || item.path); - }); + if (currentListType === 'documentSet') { + vscode.postMessage({ + command: 'saveDocumentSet', + documents: selected + }); + } else { + selected.forEach(item => { + addContextItem(currentListType === 'documentSetItem' ? 'file' : currentListType, item.path, item.relativePath || item.path); + }); + } } } finally { const menu = document.getElementById('contextMenu'); @@ -507,6 +553,11 @@ export function getContextButtonScript(): string { toggleContextMenu(); } + // 添加文档集 + function handleAddDocumentSet() { + showDocumentSetList(); + } + // 搜索功能 const searchInput = document.getElementById('contextMenuSearch'); if (searchInput) { @@ -527,6 +578,10 @@ export function getContextButtonScript(): string { switchToListView('选择文件', 'file', message.files); } else if (message.command === 'showWorkspaceFolderList') { switchToListView('选择文件夹', 'folder', message.folders); + } else if (message.command === 'showWorkspaceDocumentSetList') { + switchToListView('选择文档', 'documentSet', message.files); + } else if (message.command === 'showDocumentSetList') { + showDocumentSetView(message.documents || []); } }); `; diff --git a/src/views/contextSettingsComponent.ts b/src/views/contextSettingsComponent.ts new file mode 100644 index 0000000..8bf683d --- /dev/null +++ b/src/views/contextSettingsComponent.ts @@ -0,0 +1,119 @@ +/** + * 上下文设置组件 + * 功能:管理文档集 + */ +import { + getDocsetDialogContent, + getDocsetDialogStyles, + getDocsetDialogScript, +} from "./docsetDialog"; + +export function getContextSettingsComponentContent(): string { + return ` +
+
+

上下文

+
+
+
+ Docs + +
+
+
+

暂无文档集

+
+
+
+
+ + ${getDocsetDialogContent()} + `; +} + +export function getContextSettingsComponentStyles(): string { + return ` + .context-settings { + padding: 20px; + } + + .context-header h3 { + margin: 0 0 20px 0; + font-size: 16px; + font-weight: 600; + } + + .context-section { + background: transparent; + border: none; + padding: 0; + } + + .context-section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px solid var(--vscode-panel-border); + } + + .context-section-header span { + font-size: 14px; + font-weight: 500; + } + + .context-add-btn { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 12px; + background: transparent; + color: var(--vscode-textLink-foreground); + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: all 0.2s ease; + } + + .context-add-btn:hover { + background: var(--vscode-toolbar-hoverBackground); + color: var(--vscode-textLink-activeForeground); + } + + .context-add-btn svg { + width: 14px; + height: 14px; + } + + .context-docs-list { + min-height: 100px; + } + + .context-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + color: var(--vscode-descriptionForeground); + } + + .context-empty p { + margin: 0 0 12px 0; + font-size: 13px; + } + + ${getDocsetDialogStyles()} + `; +} + +export function getContextSettingsComponentScript(): string { + return getDocsetDialogScript(); +} diff --git a/src/views/docsetDialog.ts b/src/views/docsetDialog.ts new file mode 100644 index 0000000..c642a4a --- /dev/null +++ b/src/views/docsetDialog.ts @@ -0,0 +1,300 @@ +/** + * 文档集对话框组件 + * 功能:添加文档集的对话框 + */ + +export function getDocsetDialogContent(): string { + return ` +
+
+
+
+

添加文档集

+ +
+
+
+ + +
+
+ +
支持 .md/.txt/.v/.sv/.pdf,单个文件最大 10 MB,文档集最大 50 MB,最多 1000 个文件
+ + +
+
+ +
+
+ `; +} + +export function getDocsetDialogStyles(): string { + return ` + .docset-dialog { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10000; + } + + .docset-dialog.active { + display: flex; + align-items: center; + justify-content: center; + } + + .docset-dialog-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + } + + .docset-dialog-content { + position: relative; + width: 90%; + max-width: 500px; + background: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 8px; + display: flex; + flex-direction: column; + max-height: 80vh; + } + + .docset-dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid var(--vscode-panel-border); + } + + .docset-dialog-header h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + } + + .docset-dialog-header button { + width: 32px; + height: 32px; + background: transparent; + border: none; + font-size: 24px; + cursor: pointer; + color: var(--vscode-foreground); + border-radius: 4px; + } + + .docset-dialog-header button:hover { + background: var(--vscode-toolbar-hoverBackground); + } + + .docset-dialog-body { + padding: 20px; + overflow-y: auto; + } + + .docset-form-group { + margin-bottom: 20px; + } + + .docset-form-group label { + display: block; + margin-bottom: 8px; + font-size: 13px; + font-weight: 500; + } + + .docset-hint { + font-size: 11px; + font-weight: 400; + color: var(--vscode-descriptionForeground); + margin-bottom: 8px; + } + + .docset-form-group input { + width: 100%; + padding: 8px 12px; + background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border); + border-radius: 4px; + color: var(--vscode-input-foreground); + font-size: 13px; + box-sizing: border-box; + } + + .docset-add-file-btn { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 12px; + background: transparent; + color: var(--vscode-textLink-foreground); + border: 1px solid var(--vscode-textLink-foreground); + border-radius: 4px; + cursor: pointer; + font-size: 12px; + } + + .docset-add-file-btn:hover { + background: var(--vscode-toolbar-hoverBackground); + } + + .docset-add-file-btn svg { + width: 14px; + height: 14px; + } + + .docset-dialog-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 16px 20px; + border-top: 1px solid var(--vscode-panel-border); + } + + .docset-dialog-footer button { + padding: 6px 16px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + } + + .docset-btn-cancel { + background: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + } + + .docset-btn-cancel:hover { + background: var(--vscode-button-secondaryHoverBackground); + } + + .docset-btn-confirm { + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + } + + .docset-btn-confirm:hover { + background: var(--vscode-button-hoverBackground); + } + `; +} + +export function getDocsetDialogScript(): string { + return ` + let docsetFiles = []; + + function openAddDocumentSetDialog() { + const dialog = document.getElementById('docsetDialog'); + if (dialog) { + dialog.classList.add('active'); + docsetFiles = []; + document.getElementById('docsetName').value = ''; + document.getElementById('addFileBtn').style.display = 'flex'; + document.getElementById('docsetFilesDisplay').style.display = 'none'; + } + } + + function closeDocsetDialog() { + const dialog = document.getElementById('docsetDialog'); + if (dialog) { + dialog.classList.remove('active'); + } + } + + function addFileToDocset() { + vscode.postMessage({ command: 'selectFilesForDocset' }); + } + + function formatFileSize(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + } + + function updateDocsetDisplay() { + if (docsetFiles.length === 0) { + document.getElementById('addFileBtn').style.display = 'flex'; + document.getElementById('docsetFilesDisplay').style.display = 'none'; + return; + } + + document.getElementById('addFileBtn').style.display = 'none'; + document.getElementById('docsetFilesDisplay').style.display = 'block'; + + const listEl = document.getElementById('docsetFilesList'); + const summaryEl = document.getElementById('docsetFilesSummary'); + + listEl.innerHTML = docsetFiles.map((file, index) => \` +
+ \${file.relativePath || file.path} + +
+ \`).join(''); + + const totalSize = docsetFiles.reduce((sum, f) => sum + (f.size || 0), 0); + summaryEl.textContent = \`已选择 \${docsetFiles.length} 个文件,总大小 \${formatFileSize(totalSize)}\`; + } + + function removeDocsetFile(index) { + docsetFiles.splice(index, 1); + updateDocsetDisplay(); + } + + function confirmDocset() { + const name = document.getElementById('docsetName').value.trim(); + if (!name) { + alert('请输入文档集名称'); + return; + } + if (docsetFiles.length === 0) { + alert('请添加至少一个文件'); + return; + } + + vscode.postMessage({ + command: 'saveDocumentSet', + name: name, + documents: docsetFiles + }); + + closeDocsetDialog(); + } + + window.addEventListener('message', event => { + const message = event.data; + if (message.command === 'filesSelectedForDocset') { + if (message.errors && message.errors.length > 0) { + alert('部分文件添加失败:\\n' + message.errors.join('\\n')); + } + docsetFiles = [...docsetFiles, ...message.files]; + updateDocsetDisplay(); + } + }); + `; +} diff --git a/src/views/settingsComponent.ts b/src/views/settingsComponent.ts index db24906..fd6513e 100644 --- a/src/views/settingsComponent.ts +++ b/src/views/settingsComponent.ts @@ -8,6 +8,11 @@ import { getRulesSettingsComponentStyles, getRulesSettingsComponentScript, } from "./rulesSettingsComponent"; +import { + getContextSettingsComponentContent, + getContextSettingsComponentStyles, + getContextSettingsComponentScript, +} from "./contextSettingsComponent"; /** * 获取设置面板的 HTML 内容 @@ -34,6 +39,9 @@ export function getSettingsComponentContent(): string { +
@@ -43,6 +51,9 @@ export function getSettingsComponentContent(): string {
${getRulesSettingsComponentContent()}
+
+ ${getContextSettingsComponentContent()} +
@@ -187,6 +198,7 @@ export function getSettingsComponentStyles(): string { ${getGeneralSettingsComponentStyles()} ${getRulesSettingsComponentStyles()} + ${getContextSettingsComponentStyles()} `; } @@ -197,6 +209,7 @@ export function getSettingsComponentScript(): string { return ` ${getGeneralSettingsComponentScript()} ${getRulesSettingsComponentScript()} + ${getContextSettingsComponentScript()} // 打开设置面板 function openSettingsModal() { @@ -237,6 +250,15 @@ export function getSettingsComponentScript(): string { }); } + // 监听打开设置标签页的消息 + window.addEventListener('message', event => { + const message = event.data; + if (message.command === 'openSettingsTab') { + openSettingsModal(); + switchSettingsTab(message.tab); + } + }); + // 阻止点击模态框内容时关闭 document.addEventListener('click', (event) => { const modalContent = document.querySelector('.settings-modal-content');