From 3f80e27f3fbed6831b1189d8dd6852e267c2c038 Mon Sep 17 00:00:00 2001 From: Alexander Sullivan Date: Thu, 30 Oct 2025 17:18:31 -0400 Subject: [PATCH 1/2] fix --- README.md | 2 + docs/architecture/scanner.md | 13 ++- docs/architecture/settings.md | 1 + example/file-types-test/test-proto.proto | 7 ++ .../subdirectory-3/README | 5 + package-lock.json | 70 ++++++++---- package.json | 2 +- src/scanner/workspaceScanner.test.ts | 61 ++++++++++ src/scanner/workspaceScanner.ts | 26 ++++- src/tree/treeProvider.test.ts | 107 ++++++++++++++++++ src/tree/treeProvider.ts | 11 +- 11 files changed, 274 insertions(+), 31 deletions(-) create mode 100644 example/file-types-test/test-proto.proto create mode 100644 example/nested-structure-test/subdirectory-3/README diff --git a/README.md b/README.md index 984a64b..d5080e2 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ This extension contributes the following settings under the `workspaceWiki` name Array of file extensions to include in the workspace wiki. +**Special Case:** If `md` or `markdown` is included, files named `README` (with no extension, case-insensitive) are also included and treated as Markdown. + ```json { "workspaceWiki.supportedExtensions": ["md", "markdown", "txt", "html", "pdf"] diff --git a/docs/architecture/scanner.md b/docs/architecture/scanner.md index a2be6c5..48ff560 100644 --- a/docs/architecture/scanner.md +++ b/docs/architecture/scanner.md @@ -9,6 +9,7 @@ The Scanner/Indexer is implemented in [`src/scanner/workspaceScanner.ts`](../../ ## How It Works - Uses `workspace.findFiles` to locate files matching supported extensions (e.g., `.md`, `.markdown`, `.txt`). +- If Markdown is a supported extension, also scans for files named `README` (with no extension, case-insensitive) and treats them as Markdown. - Applies exclude patterns from settings and `.gitignore`. - Filters hidden files/folders (starting with dot) based on `showHiddenFiles` setting. - Filters ignored files based on `showIgnoredFiles` setting and exclude patterns. @@ -18,10 +19,11 @@ The Scanner/Indexer is implemented in [`src/scanner/workspaceScanner.ts`](../../ ## File Filtering Logic 1. **Extension Matching**: Only includes files with supported extensions -2. **Exclude Pattern Filtering**: Applies `excludeGlobs` and `.gitignore` patterns -3. **Hidden File Filtering**: Excludes files/folders starting with `.` unless `showHiddenFiles` is true -4. **Ignored File Filtering**: Excludes files in `.gitignore` unless `showIgnoredFiles` is true -5. **Depth Limiting**: Respects `maxSearchDepth` setting by calculating depth relative to workspace root +2. **README (no extension) Matching**: If Markdown is supported, also includes files named `README` (no extension, case-insensitive) as Markdown +3. **Exclude Pattern Filtering**: Applies `excludeGlobs` and `.gitignore` patterns +4. **Hidden File Filtering**: Excludes files/folders starting with `.` unless `showHiddenFiles` is true +5. **Ignored File Filtering**: Excludes files in `.gitignore` unless `showIgnoredFiles` is true +6. **Depth Limiting**: Respects `maxSearchDepth` setting by calculating depth relative to workspace root ## Depth Calculation @@ -36,7 +38,10 @@ The scanner calculates file depth relative to the workspace root for the `maxSea ## Example ```ts +// Standard scan for supported extensions const files = await vscode.workspace.findFiles('**/*.{md,markdown,txt}', '**/node_modules/**'); +// Additionally, if Markdown is supported, scan for README (no extension) +const readmes = await vscode.workspace.findFiles('**/README', '**/node_modules/**'); ``` ## Edge Cases diff --git a/docs/architecture/settings.md b/docs/architecture/settings.md index daee17b..5297471 100644 --- a/docs/architecture/settings.md +++ b/docs/architecture/settings.md @@ -7,6 +7,7 @@ The Settings Manager reads and applies user configuration for the Workspace Wiki ### File Discovery & Filtering - `workspaceWiki.supportedExtensions`: File types to scan (default: `md`, `markdown`, `txt`). + - If `md` or `markdown` is included, files named `README` (no extension, case-insensitive) are also included and treated as Markdown. - `workspaceWiki.excludeGlobs`: Patterns to exclude (e.g., `**/node_modules/**`). - `workspaceWiki.maxSearchDepth`: Limit scan depth for large repos. - `workspaceWiki.showIgnoredFiles`: Show files listed in .gitignore and excludeGlobs (default: false). diff --git a/example/file-types-test/test-proto.proto b/example/file-types-test/test-proto.proto new file mode 100644 index 0000000..ad156e5 --- /dev/null +++ b/example/file-types-test/test-proto.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +// test-proto.proto for file type support testing +message TestProto { + string id = 1; + string name = 2; +} diff --git a/example/nested-structure-test/subdirectory-3/README b/example/nested-structure-test/subdirectory-3/README new file mode 100644 index 0000000..10420b7 --- /dev/null +++ b/example/nested-structure-test/subdirectory-3/README @@ -0,0 +1,5 @@ +# README + +This is a README file with no extension, for testing Workspace Wiki support for extension-less Markdown files. + +It should be detected and treated as a Markdown file if Markdown is supported in the extension settings. diff --git a/package-lock.json b/package-lock.json index 96499e3..31b9e2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "workspace-wiki", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "workspace-wiki", - "version": "1.0.3", + "version": "1.0.4", "devDependencies": { "@testing-library/jest-dom": "^6.9.1", "@trivago/prettier-plugin-sort-imports": "^5.2.2", @@ -1447,13 +1447,26 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", - "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0" + "@types/json-schema": "^7.0.15" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1598,19 +1611,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -4177,9 +4203,9 @@ "optional": true }, "node_modules/baseline-browser-mapping": { - "version": "2.8.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz", - "integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==", + "version": "2.8.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.21.tgz", + "integrity": "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5392,9 +5418,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.242", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.242.tgz", - "integrity": "sha512-msZ7SYGFpXkm/iUizlMrm/FPNeYo8uSltQccLVFO3fV4RN2JWGdG7Aatztxtw3uDWp3DkupfkrosLjUnhY+iOw==", + "version": "1.5.244", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", + "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", "dev": true, "license": "ISC" }, @@ -9922,9 +9948,9 @@ "license": "MIT" }, "node_modules/node-abi": { - "version": "3.79.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.79.0.tgz", - "integrity": "sha512-Pr/5KdBQGG8TirdkS0qN3B+f3eo8zTOfZQWAxHoJqopMz2/uvRnG+S4fWu/6AZxKei2CP2p/psdQ5HFC2Ap5BA==", + "version": "3.80.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.80.0.tgz", + "integrity": "sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA==", "dev": true, "license": "MIT", "optional": true, @@ -9951,9 +9977,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", - "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index a41e27c..8cc383c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "workspace-wiki", "displayName": "Workspace Wiki", "description": "Workspace Wiki", - "version": "1.0.3", + "version": "1.0.4", "publisher": "alexjsully", "engines": { "vscode": "^1.99.3" diff --git a/src/scanner/workspaceScanner.test.ts b/src/scanner/workspaceScanner.test.ts index 6a25b19..eab8a47 100644 --- a/src/scanner/workspaceScanner.test.ts +++ b/src/scanner/workspaceScanner.test.ts @@ -35,6 +35,67 @@ describe('workspaceScanner', () => { }); describe('Supported Extensions', () => { + it('should include README (no extension) if Markdown is supported', async () => { + let calledPatterns: string[] = []; + const mockWorkspace: WorkspaceLike = { + findFiles: async (pattern: string) => { + calledPatterns.push(pattern); + // Simulate README (no extension) file + if (pattern === '**/README' || pattern === '**/readme') { + return [ + { fsPath: '/project-root/README' }, + { fsPath: '/project-root/docs/README' }, + { fsPath: '/project-root/docs/readme' }, + ]; + } + return []; + }, + getConfiguration: () => ({ + get: (key: string) => { + if (key === 'supportedExtensions') { + return ['md', 'markdown', 'txt']; + } + return undefined; + }, + }), + }; + + const result = await scanWorkspaceDocs(mockWorkspace); + // Should include all README (no extension) files + const readmeFiles = result.filter((uri: any) => /README$/i.test(uri.fsPath)); + assert.ok(readmeFiles.length >= 3, 'Should detect README files with no extension'); + // Should call the README patterns + assert.ok(calledPatterns.includes('**/README')); + assert.ok(calledPatterns.includes('**/readme')); + }); + + it('should NOT include README (no extension) if Markdown is NOT supported', async () => { + let calledPatterns: string[] = []; + const mockWorkspace: WorkspaceLike = { + findFiles: async (pattern: string) => { + calledPatterns.push(pattern); + if (pattern === '**/README' || pattern === '**/readme') { + // Should not be called + throw new Error('README pattern should not be called if Markdown is not supported'); + } + return []; + }, + getConfiguration: () => ({ + get: (key: string) => { + if (key === 'supportedExtensions') { + return ['txt', 'html']; + } + return undefined; + }, + }), + }; + + await scanWorkspaceDocs(mockWorkspace); + // Should NOT call the README patterns + assert.ok(!calledPatterns.includes('**/README')); + assert.ok(!calledPatterns.includes('**/readme')); + }); + it('should scan default extensions (md, markdown, txt)', async () => { const patterns: string[] = []; const mockWorkspace: WorkspaceLike = { diff --git a/src/scanner/workspaceScanner.ts b/src/scanner/workspaceScanner.ts index 4db4920..9405edc 100644 --- a/src/scanner/workspaceScanner.ts +++ b/src/scanner/workspaceScanner.ts @@ -11,6 +11,7 @@ export async function scanWorkspaceDocs(workspace: WorkspaceLike): Promise `**/*.${ext}`); + // Add README (no extension) support if Markdown is enabled + let patterns = supportedExtensions.map((ext) => `**/*.${ext}`); + + const markdownExts = ['md', 'markdown']; + const hasMarkdown = supportedExtensions.some((ext) => markdownExts.includes(ext.toLowerCase())); + + if (hasMarkdown) { + // README (no extension) at any depth, case-insensitive + patterns.push('**/README'); + patterns.push('**/readme'); + } + const exclude = !showIgnoredFiles && excludeGlobs.length > 0 ? `{${excludeGlobs.join(',')}}` : undefined; const results: any[] = []; @@ -80,6 +92,15 @@ export async function scanWorkspaceDocs(workspace: WorkspaceLike): Promise { + const fileName = uri.fsPath.split(/[\\/]/).pop() || ''; + return /^readme$/i.test(fileName); + }); + } + if (!showIgnoredFiles && excludeGlobs.length > 0) { uris = uris.filter((uri: any) => { const shouldExclude = excludeGlobs.some((glob) => { @@ -90,12 +111,14 @@ export async function scanWorkspaceDocs(workspace: WorkspaceLike): Promise { const segments = uri.fsPath.split(/[\\/]/); return !segments.some((seg: string) => seg.startsWith('.') && seg.length > 1); }); } + if (maxSearchDepth > 0) { uris = uris.filter((uri: any) => { const normalizedPath = uri.fsPath.replace(/\\/g, '/'); @@ -141,5 +164,6 @@ export async function scanWorkspaceDocs(workspace: WorkspaceLike): Promise { }); describe('createTreeItem', () => { + it('should set preview command for README (no extension) using md openWith', async () => { + const mockConfig = { + defaultOpenMode: 'preview', + openWith: { + md: 'markdown.showPreview', + txt: 'vscode.open', + }, + }; + mockWorkspace = createMockWorkspace(mockConfig); + provider = new WorkspaceWikiTreeProvider( + mockWorkspace, + mockTreeItem, + mockCollapsibleState, + mockEventEmitter, + ); + + const mockNode: MockTreeNode = { + type: 'file', + name: 'README', + title: 'README', + path: '/workspace-root/README', + uri: createMockUri('/workspace-root/README'), + }; + + mockScanWorkspaceDocs.mockResolvedValue([]); + mockBuildTree.mockReturnValue([mockNode]); + + await provider.getChildren(); + + // The created tree item should use markdown.showPreview as the command + expect(mockTreeItem).toHaveBeenCalledWith('README', mockCollapsibleState.None); + const createdItem = mockTreeItem.mock.results[0].value; + expect(createdItem.command).toBeDefined(); + expect(createdItem.command.arguments[1]).toBe('markdown.showPreview'); + }); + + it('should set preview command for README (no extension) using markdown openWith if md missing', async () => { + const mockConfig = { + defaultOpenMode: 'preview', + openWith: { + markdown: 'markdown.customPreview', + txt: 'vscode.open', + }, + }; + mockWorkspace = createMockWorkspace(mockConfig); + provider = new WorkspaceWikiTreeProvider( + mockWorkspace, + mockTreeItem, + mockCollapsibleState, + mockEventEmitter, + ); + + const mockNode: MockTreeNode = { + type: 'file', + name: 'README', + title: 'README', + path: '/workspace-root/README', + uri: createMockUri('/workspace-root/README'), + }; + + mockScanWorkspaceDocs.mockResolvedValue([]); + mockBuildTree.mockReturnValue([mockNode]); + + await provider.getChildren(); + + // The created tree item should use markdown.customPreview as the command + expect(mockTreeItem).toHaveBeenCalledWith('README', mockCollapsibleState.None); + const createdItem = mockTreeItem.mock.results[0].value; + expect(createdItem.command).toBeDefined(); + expect(createdItem.command.arguments[1]).toBe('markdown.customPreview'); + }); + + it('should fallback to markdown.showPreview for README (no extension) if no openWith entry', async () => { + const mockConfig = { + defaultOpenMode: 'preview', + openWith: { + txt: 'vscode.open', + }, + }; + mockWorkspace = createMockWorkspace(mockConfig); + provider = new WorkspaceWikiTreeProvider( + mockWorkspace, + mockTreeItem, + mockCollapsibleState, + mockEventEmitter, + ); + + const mockNode: MockTreeNode = { + type: 'file', + name: 'README', + title: 'README', + path: '/workspace-root/README', + uri: createMockUri('/workspace-root/README'), + }; + + mockScanWorkspaceDocs.mockResolvedValue([]); + mockBuildTree.mockReturnValue([mockNode]); + + await provider.getChildren(); + + // The created tree item should fallback to markdown.showPreview + expect(mockTreeItem).toHaveBeenCalledWith('README', mockCollapsibleState.None); + const createdItem = mockTreeItem.mock.results[0].value; + expect(createdItem.command).toBeDefined(); + expect(createdItem.command.arguments[1]).toBe('markdown.showPreview'); + }); + it('should create tree item for file nodes', async () => { const mockNode: MockTreeNode = { type: 'file', diff --git a/src/tree/treeProvider.ts b/src/tree/treeProvider.ts index 67ebe93..bb9d335 100644 --- a/src/tree/treeProvider.ts +++ b/src/tree/treeProvider.ts @@ -114,11 +114,16 @@ export class WorkspaceWikiTreeProvider { } // Determine which command to use for default click - const fileExt = node.name.split('.').pop()?.toLowerCase(); + let fileExt = node.name.split('.').pop()?.toLowerCase(); let defaultCommand = 'vscode.open'; - if (defaultOpenMode === 'preview' && fileExt && openWith[fileExt]) { - defaultCommand = openWith[fileExt]; + // Special case: README (no extension) should always use md/markdown preview if in preview mode + if (defaultOpenMode === 'preview') { + if (!node.name.includes('.') && node.name.toLowerCase() === 'readme') { + defaultCommand = openWith['md'] || openWith['markdown'] || 'markdown.showPreview'; + } else if (fileExt && openWith[fileExt]) { + defaultCommand = openWith[fileExt]; + } } item.command = { From 81ca70ee8938208310b0b726697abc0cb263e87e Mon Sep 17 00:00:00 2001 From: Alexander Sullivan Date: Thu, 30 Oct 2025 17:33:58 -0400 Subject: [PATCH 2/2] updated changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0519916..93ed9f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p To see tags and releases, please go to [Tags](https://github.com/AlexJSully/workspace-wiki/tags) on [GitHub](https://github.com/AlexJSully/workspace-wiki). +## [1.0.4] - 2025-10-30 + +Bug Fix: + +- Fixed a bug where README files with no extension were not appearing in the Workspace Wiki tree even when Markdown was a supported extension. + ## [1.0.3] - 2025-10-28 Features: