jupyterlab_vscode_icons_extension 1.1.8 → 1.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/index.js +88 -66
- package/lib/parsers.d.ts +16 -0
- package/lib/parsers.js +47 -0
- package/package.json +1 -1
- package/src/__tests__/parsers.spec.ts +342 -0
- package/src/index.ts +97 -69
- package/src/parsers.ts +57 -0
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ This extension brings 1414 beautiful file type icons from the vscode-icons proje
|
|
|
21
21
|
- Zero configuration required - just install and enjoy
|
|
22
22
|
- Lightweight integration using Iconify's JSON icon format
|
|
23
23
|
- Compatible with Jupytext - properly displays Python and Markdown icons for .py and .md notebook files
|
|
24
|
-
-
|
|
24
|
+
- Python package folder detection - folders declared in `pyproject.toml` or `setup.py` get a special Python package icon
|
|
25
25
|
|
|
26
26
|
## Requirements
|
|
27
27
|
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
|
2
2
|
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
3
3
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
4
4
|
import { getIconSVG } from './icons';
|
|
5
|
+
import { parsePyprojectToml, parseSetupPy } from './parsers';
|
|
5
6
|
const PLUGIN_ID = 'jupyterlab_vscode_icons_extension:plugin';
|
|
6
7
|
/**
|
|
7
8
|
* Create a LabIcon from vscode-icons SVG data
|
|
@@ -442,6 +443,8 @@ const plugin = {
|
|
|
442
443
|
const pytestDataUri = `data:image/svg+xml;base64,${btoa(pytestSvg)}`;
|
|
443
444
|
const pythonPackageSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><linearGradient id="ppa" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0" stop-color="#387eb8"/><stop offset="1" stop-color="#366994"/></linearGradient><linearGradient id="ppb" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0" stop-color="#ffe052"/><stop offset="1" stop-color="#ffc331"/></linearGradient></defs><g transform="scale(-1,1) translate(-32,0)"><path d="M27.4,5.5H18.1L16,9.7H4.3V26.5H29.5V5.5Zm0,4.2H19.2l1.1-2.1h7.1Z" fill="#58af7b"/><path d="M20.9,11c-5.1,0-4.8,2.2-4.8,2.2v2.3H21v.7H14.2S11,15.8,11,21s2.9,5,2.9,5h1.7V23.6a2.7,2.7,0,0,1,2.8-2.9h4.8a2.6,2.6,0,0,0,2.7-2.6V13.7S26.2,11,20.9,11Zm-2.7,1.5a.9.9,0,1,1-.8.9.9.9,0,0,1,.8-.9Z" fill="url(#ppa)"/><path d="M21.1,31c5.1,0,4.8-2.2,4.8-2.2V26.5H21v-.7h6.8S31,26.1,31,21s-2.9-5-2.9-5h-1.7v2.4a2.7,2.7,0,0,1-2.8,2.9H18.8a2.6,2.6,0,0,0-2.7,2.6v4.4S15.7,31,21,31Zm2.7-1.5a.9.9,0,1,1,.8-.9.9.9,0,0,1-.8.9Z" fill="url(#ppb)"/></g></svg>';
|
|
444
445
|
const pythonPackageDataUri = `data:image/svg+xml;base64,${btoa(pythonPackageSvg)}`;
|
|
446
|
+
const venvSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g transform="scale(-1,1) translate(-32,0)"><path d="M27.4,5.5H18.1L16,9.7H4.3V26.5H29.5V5.5Zm0,4.2H19.2l1.1-2.1h7.1Z" fill="#9575cd"/><g transform="translate(22,22) scale(1.25)" fill="#bababa"><path d="M-1.2,-6 L1.2,-6 L1.5,-4.5 L2.8,-4 L4,-5 L5.5,-3.5 L4.5,-2.3 L5,-1 L6.5,-0.8 L6.5,1.2 L5,1.5 L4.5,2.8 L5.5,4 L4,5.5 L2.8,4.5 L1.5,5 L1.2,6.5 L-1.2,6.5 L-1.5,5 L-2.8,4.5 L-4,5.5 L-5.5,4 L-4.5,2.8 L-5,1.5 L-6.5,1.2 L-6.5,-0.8 L-5,-1 L-4.5,-2.3 L-5.5,-3.5 L-4,-5 L-2.8,-4 L-1.5,-4.5 Z"/><circle cx="0" cy="0" r="2.5" fill="#9575cd"/></g></g></svg>';
|
|
447
|
+
const venvDataUri = `data:image/svg+xml;base64,${btoa(venvSvg)}`;
|
|
445
448
|
// Inject CSS that overrides icons for .py and .md files
|
|
446
449
|
// Note: Jupytext marks .py and .md files as type="notebook", so we need to
|
|
447
450
|
// use JavaScript to detect and mark these files for CSS targeting
|
|
@@ -645,6 +648,22 @@ const plugin = {
|
|
|
645
648
|
background-repeat: no-repeat;
|
|
646
649
|
background-position: center;
|
|
647
650
|
}
|
|
651
|
+
|
|
652
|
+
/* Override venv folder icons (.venv, venv, .env, env) */
|
|
653
|
+
.jp-DirListing-item[data-venv-folder] .jp-DirListing-itemIcon svg,
|
|
654
|
+
.jp-DirListing-item[data-venv-folder] .jp-DirListing-itemIcon img {
|
|
655
|
+
display: none !important;
|
|
656
|
+
}
|
|
657
|
+
.jp-DirListing-item[data-venv-folder] .jp-DirListing-itemIcon::before {
|
|
658
|
+
content: '';
|
|
659
|
+
display: inline-block;
|
|
660
|
+
width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
|
|
661
|
+
height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
|
|
662
|
+
background-image: url('${venvDataUri}');
|
|
663
|
+
background-size: contain;
|
|
664
|
+
background-repeat: no-repeat;
|
|
665
|
+
background-position: center;
|
|
666
|
+
}
|
|
648
667
|
`;
|
|
649
668
|
// Add CSS to make JavaScript and .env icons less bright
|
|
650
669
|
style.textContent += `
|
|
@@ -666,71 +685,60 @@ const plugin = {
|
|
|
666
685
|
opacity: 55% !important;
|
|
667
686
|
}
|
|
668
687
|
`;
|
|
669
|
-
// Cache for Python package
|
|
670
|
-
|
|
671
|
-
//
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
/^venv$/,
|
|
678
|
-
/^\.env$/,
|
|
679
|
-
/^env$/,
|
|
680
|
-
/^node_modules$/,
|
|
681
|
-
/^__pycache__$/,
|
|
682
|
-
/^\.ipynb_checkpoints$/,
|
|
683
|
-
/^\.pytest_cache$/,
|
|
684
|
-
/^\.mypy_cache$/,
|
|
685
|
-
/^\.ruff_cache$/,
|
|
686
|
-
/^\.tox$/,
|
|
687
|
-
/^\.nox$/,
|
|
688
|
-
/^\.coverage$/,
|
|
689
|
-
/^htmlcov$/,
|
|
690
|
-
/^dist$/,
|
|
691
|
-
/^build$/,
|
|
692
|
-
/^\.eggs$/,
|
|
693
|
-
/\.egg-info$/,
|
|
694
|
-
/\.dist-info$/
|
|
695
|
-
];
|
|
696
|
-
const isExcludedFolder = (name) => {
|
|
697
|
-
return excludedFolderPatterns.some(pattern => pattern.test(name));
|
|
698
|
-
};
|
|
699
|
-
// Check if a folder is a Python package (contains __init__.py)
|
|
700
|
-
// Use the file browser's model to get the correct path
|
|
701
|
-
const checkPythonPackage = async (folderName) => {
|
|
702
|
-
var _a;
|
|
703
|
-
// Skip excluded folders
|
|
704
|
-
if (isExcludedFolder(folderName)) {
|
|
705
|
-
return false;
|
|
706
|
-
}
|
|
707
|
-
// Get the current path from the file browser model
|
|
708
|
-
if (!defaultFileBrowser) {
|
|
709
|
-
return false;
|
|
710
|
-
}
|
|
711
|
-
const currentPath = defaultFileBrowser.model.path;
|
|
712
|
-
const fullPath = currentPath
|
|
713
|
-
? `${currentPath}/${folderName}`
|
|
714
|
-
: folderName;
|
|
715
|
-
if (pythonPackageCache.has(fullPath)) {
|
|
716
|
-
return pythonPackageCache.get(fullPath);
|
|
688
|
+
// Cache for Python package detection (per directory)
|
|
689
|
+
let pythonPackagesCache = null;
|
|
690
|
+
// Detect Python packages by parsing pyproject.toml or setup.py
|
|
691
|
+
const detectPythonPackages = async () => {
|
|
692
|
+
const currentPath = (defaultFileBrowser === null || defaultFileBrowser === void 0 ? void 0 : defaultFileBrowser.model.path) || '';
|
|
693
|
+
// Return cached result if same directory
|
|
694
|
+
if ((pythonPackagesCache === null || pythonPackagesCache === void 0 ? void 0 : pythonPackagesCache.path) === currentPath) {
|
|
695
|
+
return pythonPackagesCache.packages;
|
|
717
696
|
}
|
|
697
|
+
const packages = new Set();
|
|
718
698
|
try {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
699
|
+
const contents = await app.serviceManager.contents.get(currentPath, {
|
|
700
|
+
content: true
|
|
701
|
+
});
|
|
702
|
+
const files = contents.content || [];
|
|
703
|
+
// Check for pyproject.toml
|
|
704
|
+
const hasPyproject = files.some((f) => f.name === 'pyproject.toml');
|
|
705
|
+
if (hasPyproject) {
|
|
706
|
+
const pyprojectPath = currentPath
|
|
707
|
+
? `${currentPath}/pyproject.toml`
|
|
708
|
+
: 'pyproject.toml';
|
|
709
|
+
const file = await app.serviceManager.contents.get(pyprojectPath);
|
|
710
|
+
const content = file.content;
|
|
711
|
+
const parsed = parsePyprojectToml(content);
|
|
712
|
+
parsed.forEach(p => packages.add(p));
|
|
713
|
+
}
|
|
714
|
+
// Check for setup.py
|
|
715
|
+
const hasSetupPy = files.some((f) => f.name === 'setup.py');
|
|
716
|
+
if (hasSetupPy) {
|
|
717
|
+
const setupPath = currentPath
|
|
718
|
+
? `${currentPath}/setup.py`
|
|
719
|
+
: 'setup.py';
|
|
720
|
+
const file = await app.serviceManager.contents.get(setupPath);
|
|
721
|
+
const content = file.content;
|
|
722
|
+
const parsed = parseSetupPy(content);
|
|
723
|
+
parsed.forEach(p => packages.add(p));
|
|
724
|
+
}
|
|
726
725
|
}
|
|
727
|
-
catch (
|
|
728
|
-
|
|
729
|
-
return false;
|
|
726
|
+
catch (_a) {
|
|
727
|
+
// Ignore errors - no packages detected
|
|
730
728
|
}
|
|
729
|
+
pythonPackagesCache = { path: currentPath, packages };
|
|
730
|
+
return packages;
|
|
731
731
|
};
|
|
732
|
+
// Invalidate cache on directory change
|
|
733
|
+
if (defaultFileBrowser) {
|
|
734
|
+
defaultFileBrowser.model.pathChanged.connect(() => {
|
|
735
|
+
pythonPackagesCache = null;
|
|
736
|
+
});
|
|
737
|
+
}
|
|
732
738
|
// Add a MutationObserver to mark special files in the file browser
|
|
733
|
-
const markSpecialFiles = () => {
|
|
739
|
+
const markSpecialFiles = async () => {
|
|
740
|
+
// Get Python packages for current directory (cached)
|
|
741
|
+
const pythonPackages = await detectPythonPackages();
|
|
734
742
|
// Process ALL items - clear wrong attributes and set correct ones
|
|
735
743
|
const allItems = document.querySelectorAll('.jp-DirListing-item');
|
|
736
744
|
allItems.forEach(item => {
|
|
@@ -813,25 +821,39 @@ const plugin = {
|
|
|
813
821
|
const isDir = fileType === 'directory' ||
|
|
814
822
|
item.classList.contains('jp-DirListing-directory');
|
|
815
823
|
if (isDir) {
|
|
816
|
-
// Check if
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
824
|
+
// Check if folder is a venv folder (.venv, venv, .env, env)
|
|
825
|
+
const venvNames = ['.venv', 'venv', '.env', 'env'];
|
|
826
|
+
if (venvNames.includes(nameLower)) {
|
|
827
|
+
item.setAttribute('data-venv-folder', 'true');
|
|
828
|
+
item.removeAttribute('data-python-package');
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
item.removeAttribute('data-venv-folder');
|
|
832
|
+
// Check if folder name matches a detected Python package
|
|
833
|
+
if (pythonPackages.has(name)) {
|
|
820
834
|
item.setAttribute('data-python-package', 'true');
|
|
821
835
|
}
|
|
822
836
|
else {
|
|
823
837
|
item.removeAttribute('data-python-package');
|
|
824
838
|
}
|
|
825
|
-
}
|
|
839
|
+
}
|
|
826
840
|
}
|
|
827
841
|
else {
|
|
828
842
|
item.removeAttribute('data-python-package');
|
|
843
|
+
item.removeAttribute('data-venv-folder');
|
|
829
844
|
}
|
|
830
845
|
});
|
|
831
846
|
};
|
|
832
|
-
//
|
|
847
|
+
// Debounce timeout for MutationObserver
|
|
848
|
+
let markSpecialFilesTimeout = null;
|
|
849
|
+
// Watch for changes in the file browser (debounced)
|
|
833
850
|
const observer = new MutationObserver(() => {
|
|
834
|
-
|
|
851
|
+
if (markSpecialFilesTimeout) {
|
|
852
|
+
clearTimeout(markSpecialFilesTimeout);
|
|
853
|
+
}
|
|
854
|
+
markSpecialFilesTimeout = setTimeout(() => {
|
|
855
|
+
markSpecialFiles();
|
|
856
|
+
}, 100);
|
|
835
857
|
});
|
|
836
858
|
// Start observing when the file browser is ready
|
|
837
859
|
setTimeout(() => {
|
package/lib/parsers.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python package configuration file parsers
|
|
3
|
+
* Extracts package names from pyproject.toml and setup.py files
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Parse pyproject.toml content to extract Python package names
|
|
7
|
+
* @param content - The content of pyproject.toml file
|
|
8
|
+
* @returns Set of package names found in the file
|
|
9
|
+
*/
|
|
10
|
+
export declare function parsePyprojectToml(content: string): Set<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Parse setup.py content to extract Python package names
|
|
13
|
+
* @param content - The content of setup.py file
|
|
14
|
+
* @returns Set of package names found in the file
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseSetupPy(content: string): Set<string>;
|
package/lib/parsers.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python package configuration file parsers
|
|
3
|
+
* Extracts package names from pyproject.toml and setup.py files
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Parse pyproject.toml content to extract Python package names
|
|
7
|
+
* @param content - The content of pyproject.toml file
|
|
8
|
+
* @returns Set of package names found in the file
|
|
9
|
+
*/
|
|
10
|
+
export function parsePyprojectToml(content) {
|
|
11
|
+
const packages = new Set();
|
|
12
|
+
// Parse [project] name = "package_name"
|
|
13
|
+
const nameMatch = content.match(/\[project\][^[]*name\s*=\s*["']([^"']+)["']/s);
|
|
14
|
+
if (nameMatch) {
|
|
15
|
+
packages.add(nameMatch[1]);
|
|
16
|
+
// Also add underscore variant (package-name -> package_name)
|
|
17
|
+
packages.add(nameMatch[1].replace(/-/g, '_'));
|
|
18
|
+
}
|
|
19
|
+
// Parse packages = ["pkg1", "pkg2"]
|
|
20
|
+
const packagesMatch = content.match(/packages\s*=\s*\[([^\]]+)\]/);
|
|
21
|
+
if (packagesMatch) {
|
|
22
|
+
const pkgList = packagesMatch[1].match(/["']([^"']+)["']/g);
|
|
23
|
+
pkgList === null || pkgList === void 0 ? void 0 : pkgList.forEach(p => packages.add(p.replace(/["']/g, '')));
|
|
24
|
+
}
|
|
25
|
+
return packages;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse setup.py content to extract Python package names
|
|
29
|
+
* @param content - The content of setup.py file
|
|
30
|
+
* @returns Set of package names found in the file
|
|
31
|
+
*/
|
|
32
|
+
export function parseSetupPy(content) {
|
|
33
|
+
const packages = new Set();
|
|
34
|
+
// Parse packages=["pkg1", "pkg2"]
|
|
35
|
+
const packagesMatch = content.match(/packages\s*=\s*\[([^\]]+)\]/);
|
|
36
|
+
if (packagesMatch) {
|
|
37
|
+
const pkgList = packagesMatch[1].match(/["']([^"']+)["']/g);
|
|
38
|
+
pkgList === null || pkgList === void 0 ? void 0 : pkgList.forEach(p => packages.add(p.replace(/["']/g, '')));
|
|
39
|
+
}
|
|
40
|
+
// Parse name="package_name"
|
|
41
|
+
const nameMatch = content.match(/name\s*=\s*["']([^"']+)["']/);
|
|
42
|
+
if (nameMatch) {
|
|
43
|
+
packages.add(nameMatch[1]);
|
|
44
|
+
packages.add(nameMatch[1].replace(/-/g, '_'));
|
|
45
|
+
}
|
|
46
|
+
return packages;
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Python package configuration file parsers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { parsePyprojectToml, parseSetupPy } from '../parsers';
|
|
6
|
+
|
|
7
|
+
describe('parsePyprojectToml', () => {
|
|
8
|
+
describe('project name parsing', () => {
|
|
9
|
+
it('should parse project name with double quotes', () => {
|
|
10
|
+
const content = `
|
|
11
|
+
[project]
|
|
12
|
+
name = "my-package"
|
|
13
|
+
version = "1.0.0"
|
|
14
|
+
`;
|
|
15
|
+
const result = parsePyprojectToml(content);
|
|
16
|
+
expect(result.has('my-package')).toBe(true);
|
|
17
|
+
expect(result.has('my_package')).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should parse project name with single quotes', () => {
|
|
21
|
+
const content = `
|
|
22
|
+
[project]
|
|
23
|
+
name = 'my-package'
|
|
24
|
+
version = '1.0.0'
|
|
25
|
+
`;
|
|
26
|
+
const result = parsePyprojectToml(content);
|
|
27
|
+
expect(result.has('my-package')).toBe(true);
|
|
28
|
+
expect(result.has('my_package')).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should handle name without hyphens', () => {
|
|
32
|
+
const content = `
|
|
33
|
+
[project]
|
|
34
|
+
name = "mypackage"
|
|
35
|
+
`;
|
|
36
|
+
const result = parsePyprojectToml(content);
|
|
37
|
+
expect(result.has('mypackage')).toBe(true);
|
|
38
|
+
// underscore variant is same as original
|
|
39
|
+
expect(result.size).toBe(1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle name with multiple hyphens', () => {
|
|
43
|
+
const content = `
|
|
44
|
+
[project]
|
|
45
|
+
name = "my-awesome-package"
|
|
46
|
+
`;
|
|
47
|
+
const result = parsePyprojectToml(content);
|
|
48
|
+
expect(result.has('my-awesome-package')).toBe(true);
|
|
49
|
+
expect(result.has('my_awesome_package')).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should handle whitespace around equals sign', () => {
|
|
53
|
+
const content = `
|
|
54
|
+
[project]
|
|
55
|
+
name="my-package"
|
|
56
|
+
`;
|
|
57
|
+
const result = parsePyprojectToml(content);
|
|
58
|
+
expect(result.has('my-package')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should handle extra whitespace', () => {
|
|
62
|
+
const content = `
|
|
63
|
+
[project]
|
|
64
|
+
name = "my-package"
|
|
65
|
+
`;
|
|
66
|
+
const result = parsePyprojectToml(content);
|
|
67
|
+
expect(result.has('my-package')).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('packages array parsing', () => {
|
|
72
|
+
it('should parse packages array with double quotes', () => {
|
|
73
|
+
const content = `
|
|
74
|
+
[tool.setuptools]
|
|
75
|
+
packages = ["pkg1", "pkg2", "pkg3"]
|
|
76
|
+
`;
|
|
77
|
+
const result = parsePyprojectToml(content);
|
|
78
|
+
expect(result.has('pkg1')).toBe(true);
|
|
79
|
+
expect(result.has('pkg2')).toBe(true);
|
|
80
|
+
expect(result.has('pkg3')).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should parse packages array with single quotes', () => {
|
|
84
|
+
const content = `
|
|
85
|
+
[tool.setuptools]
|
|
86
|
+
packages = ['pkg1', 'pkg2']
|
|
87
|
+
`;
|
|
88
|
+
const result = parsePyprojectToml(content);
|
|
89
|
+
expect(result.has('pkg1')).toBe(true);
|
|
90
|
+
expect(result.has('pkg2')).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should parse packages array with mixed quotes', () => {
|
|
94
|
+
const content = `
|
|
95
|
+
packages = ["pkg1", 'pkg2']
|
|
96
|
+
`;
|
|
97
|
+
const result = parsePyprojectToml(content);
|
|
98
|
+
expect(result.has('pkg1')).toBe(true);
|
|
99
|
+
expect(result.has('pkg2')).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should parse multiline packages array', () => {
|
|
103
|
+
const content = `
|
|
104
|
+
packages = [
|
|
105
|
+
"pkg1",
|
|
106
|
+
"pkg2",
|
|
107
|
+
"pkg3"
|
|
108
|
+
]
|
|
109
|
+
`;
|
|
110
|
+
const result = parsePyprojectToml(content);
|
|
111
|
+
expect(result.has('pkg1')).toBe(true);
|
|
112
|
+
expect(result.has('pkg2')).toBe(true);
|
|
113
|
+
expect(result.has('pkg3')).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should parse single package in array', () => {
|
|
117
|
+
const content = `
|
|
118
|
+
packages = ["single_pkg"]
|
|
119
|
+
`;
|
|
120
|
+
const result = parsePyprojectToml(content);
|
|
121
|
+
expect(result.has('single_pkg')).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('combined parsing', () => {
|
|
126
|
+
it('should parse both project name and packages array', () => {
|
|
127
|
+
const content = `
|
|
128
|
+
[project]
|
|
129
|
+
name = "main-package"
|
|
130
|
+
version = "1.0.0"
|
|
131
|
+
|
|
132
|
+
[tool.setuptools]
|
|
133
|
+
packages = ["subpkg1", "subpkg2"]
|
|
134
|
+
`;
|
|
135
|
+
const result = parsePyprojectToml(content);
|
|
136
|
+
expect(result.has('main-package')).toBe(true);
|
|
137
|
+
expect(result.has('main_package')).toBe(true);
|
|
138
|
+
expect(result.has('subpkg1')).toBe(true);
|
|
139
|
+
expect(result.has('subpkg2')).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should handle real-world pyproject.toml', () => {
|
|
143
|
+
const content = `
|
|
144
|
+
[build-system]
|
|
145
|
+
requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5"]
|
|
146
|
+
build-backend = "hatchling.build"
|
|
147
|
+
|
|
148
|
+
[project]
|
|
149
|
+
name = "jupyterlab_vscode_icons_extension"
|
|
150
|
+
version = "1.1.9"
|
|
151
|
+
description = "VSCode-style file icons for JupyterLab"
|
|
152
|
+
readme = "README.md"
|
|
153
|
+
license = { file = "LICENSE" }
|
|
154
|
+
requires-python = ">=3.8"
|
|
155
|
+
classifiers = [
|
|
156
|
+
"Framework :: Jupyter",
|
|
157
|
+
"Framework :: Jupyter :: JupyterLab",
|
|
158
|
+
"Framework :: Jupyter :: JupyterLab :: 4",
|
|
159
|
+
"License :: OSI Approved :: BSD License",
|
|
160
|
+
"Programming Language :: Python",
|
|
161
|
+
"Programming Language :: Python :: 3",
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
[tool.hatch.build.targets.wheel.shared-data]
|
|
165
|
+
"jupyterlab_vscode_icons_extension/labextension" = "share/jupyter/labextensions/jupyterlab_vscode_icons_extension"
|
|
166
|
+
`;
|
|
167
|
+
const result = parsePyprojectToml(content);
|
|
168
|
+
expect(result.has('jupyterlab_vscode_icons_extension')).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('edge cases', () => {
|
|
173
|
+
it('should return empty set for empty content', () => {
|
|
174
|
+
const result = parsePyprojectToml('');
|
|
175
|
+
expect(result.size).toBe(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should return empty set for content without project section', () => {
|
|
179
|
+
const content = `
|
|
180
|
+
[build-system]
|
|
181
|
+
requires = ["hatchling"]
|
|
182
|
+
`;
|
|
183
|
+
const result = parsePyprojectToml(content);
|
|
184
|
+
expect(result.size).toBe(0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should not match name outside [project] section', () => {
|
|
188
|
+
const content = `
|
|
189
|
+
[other-section]
|
|
190
|
+
name = "wrong-package"
|
|
191
|
+
|
|
192
|
+
[project]
|
|
193
|
+
version = "1.0.0"
|
|
194
|
+
`;
|
|
195
|
+
const result = parsePyprojectToml(content);
|
|
196
|
+
expect(result.has('wrong-package')).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('parseSetupPy', () => {
|
|
202
|
+
describe('name parsing', () => {
|
|
203
|
+
it('should parse name with double quotes', () => {
|
|
204
|
+
const content = `
|
|
205
|
+
from setuptools import setup
|
|
206
|
+
|
|
207
|
+
setup(
|
|
208
|
+
name="my-package",
|
|
209
|
+
version="1.0.0",
|
|
210
|
+
)
|
|
211
|
+
`;
|
|
212
|
+
const result = parseSetupPy(content);
|
|
213
|
+
expect(result.has('my-package')).toBe(true);
|
|
214
|
+
expect(result.has('my_package')).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should parse name with single quotes', () => {
|
|
218
|
+
const content = `
|
|
219
|
+
setup(
|
|
220
|
+
name='my-package',
|
|
221
|
+
)
|
|
222
|
+
`;
|
|
223
|
+
const result = parseSetupPy(content);
|
|
224
|
+
expect(result.has('my-package')).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should handle name without hyphens', () => {
|
|
228
|
+
const content = `
|
|
229
|
+
setup(name="mypackage")
|
|
230
|
+
`;
|
|
231
|
+
const result = parseSetupPy(content);
|
|
232
|
+
expect(result.has('mypackage')).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('packages array parsing', () => {
|
|
237
|
+
it('should parse packages array inline', () => {
|
|
238
|
+
const content = `
|
|
239
|
+
setup(
|
|
240
|
+
packages=["pkg1", "pkg2", "pkg3"],
|
|
241
|
+
)
|
|
242
|
+
`;
|
|
243
|
+
const result = parseSetupPy(content);
|
|
244
|
+
expect(result.has('pkg1')).toBe(true);
|
|
245
|
+
expect(result.has('pkg2')).toBe(true);
|
|
246
|
+
expect(result.has('pkg3')).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should parse packages array multiline', () => {
|
|
250
|
+
const content = `
|
|
251
|
+
setup(
|
|
252
|
+
packages=[
|
|
253
|
+
"pkg1",
|
|
254
|
+
"pkg2",
|
|
255
|
+
],
|
|
256
|
+
)
|
|
257
|
+
`;
|
|
258
|
+
const result = parseSetupPy(content);
|
|
259
|
+
expect(result.has('pkg1')).toBe(true);
|
|
260
|
+
expect(result.has('pkg2')).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should parse packages with single quotes', () => {
|
|
264
|
+
const content = `
|
|
265
|
+
packages=['pkg1', 'pkg2']
|
|
266
|
+
`;
|
|
267
|
+
const result = parseSetupPy(content);
|
|
268
|
+
expect(result.has('pkg1')).toBe(true);
|
|
269
|
+
expect(result.has('pkg2')).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('combined parsing', () => {
|
|
274
|
+
it('should parse both name and packages', () => {
|
|
275
|
+
const content = `
|
|
276
|
+
from setuptools import setup
|
|
277
|
+
|
|
278
|
+
setup(
|
|
279
|
+
name="main-package",
|
|
280
|
+
version="1.0.0",
|
|
281
|
+
packages=["subpkg1", "subpkg2"],
|
|
282
|
+
)
|
|
283
|
+
`;
|
|
284
|
+
const result = parseSetupPy(content);
|
|
285
|
+
expect(result.has('main-package')).toBe(true);
|
|
286
|
+
expect(result.has('main_package')).toBe(true);
|
|
287
|
+
expect(result.has('subpkg1')).toBe(true);
|
|
288
|
+
expect(result.has('subpkg2')).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should handle real-world setup.py', () => {
|
|
292
|
+
const content = `
|
|
293
|
+
#!/usr/bin/env python
|
|
294
|
+
from setuptools import setup, find_packages
|
|
295
|
+
|
|
296
|
+
setup(
|
|
297
|
+
name="my-awesome-lib",
|
|
298
|
+
version="2.0.0",
|
|
299
|
+
author="Developer",
|
|
300
|
+
author_email="dev@example.com",
|
|
301
|
+
description="An awesome library",
|
|
302
|
+
packages=find_packages(),
|
|
303
|
+
python_requires=">=3.8",
|
|
304
|
+
install_requires=[
|
|
305
|
+
"numpy>=1.0",
|
|
306
|
+
"pandas>=1.0",
|
|
307
|
+
],
|
|
308
|
+
)
|
|
309
|
+
`;
|
|
310
|
+
const result = parseSetupPy(content);
|
|
311
|
+
expect(result.has('my-awesome-lib')).toBe(true);
|
|
312
|
+
expect(result.has('my_awesome_lib')).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('edge cases', () => {
|
|
317
|
+
it('should return empty set for empty content', () => {
|
|
318
|
+
const result = parseSetupPy('');
|
|
319
|
+
expect(result.size).toBe(0);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should return empty set for content with find_packages() only', () => {
|
|
323
|
+
const content = `
|
|
324
|
+
setup(
|
|
325
|
+
packages=find_packages(),
|
|
326
|
+
)
|
|
327
|
+
`;
|
|
328
|
+
const result = parseSetupPy(content);
|
|
329
|
+
// find_packages() is dynamic, not a literal array
|
|
330
|
+
expect(result.size).toBe(0);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should handle whitespace variations', () => {
|
|
334
|
+
const content = `
|
|
335
|
+
setup(name = "my-pkg",packages = ["pkg1"])
|
|
336
|
+
`;
|
|
337
|
+
const result = parseSetupPy(content);
|
|
338
|
+
expect(result.has('my-pkg')).toBe(true);
|
|
339
|
+
expect(result.has('pkg1')).toBe(true);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
|
6
6
|
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
7
7
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
8
8
|
import { getIconSVG } from './icons';
|
|
9
|
+
import { parsePyprojectToml, parseSetupPy } from './parsers';
|
|
9
10
|
|
|
10
11
|
const PLUGIN_ID = 'jupyterlab_vscode_icons_extension:plugin';
|
|
11
12
|
|
|
@@ -494,6 +495,9 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
494
495
|
const pythonPackageSvg =
|
|
495
496
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><linearGradient id="ppa" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0" stop-color="#387eb8"/><stop offset="1" stop-color="#366994"/></linearGradient><linearGradient id="ppb" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0" stop-color="#ffe052"/><stop offset="1" stop-color="#ffc331"/></linearGradient></defs><g transform="scale(-1,1) translate(-32,0)"><path d="M27.4,5.5H18.1L16,9.7H4.3V26.5H29.5V5.5Zm0,4.2H19.2l1.1-2.1h7.1Z" fill="#58af7b"/><path d="M20.9,11c-5.1,0-4.8,2.2-4.8,2.2v2.3H21v.7H14.2S11,15.8,11,21s2.9,5,2.9,5h1.7V23.6a2.7,2.7,0,0,1,2.8-2.9h4.8a2.6,2.6,0,0,0,2.7-2.6V13.7S26.2,11,20.9,11Zm-2.7,1.5a.9.9,0,1,1-.8.9.9.9,0,0,1,.8-.9Z" fill="url(#ppa)"/><path d="M21.1,31c5.1,0,4.8-2.2,4.8-2.2V26.5H21v-.7h6.8S31,26.1,31,21s-2.9-5-2.9-5h-1.7v2.4a2.7,2.7,0,0,1-2.8,2.9H18.8a2.6,2.6,0,0,0-2.7,2.6v4.4S15.7,31,21,31Zm2.7-1.5a.9.9,0,1,1,.8-.9.9.9,0,0,1-.8.9Z" fill="url(#ppb)"/></g></svg>';
|
|
496
497
|
const pythonPackageDataUri = `data:image/svg+xml;base64,${btoa(pythonPackageSvg)}`;
|
|
498
|
+
const venvSvg =
|
|
499
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g transform="scale(-1,1) translate(-32,0)"><path d="M27.4,5.5H18.1L16,9.7H4.3V26.5H29.5V5.5Zm0,4.2H19.2l1.1-2.1h7.1Z" fill="#9575cd"/><g transform="translate(22,22) scale(1.25)" fill="#bababa"><path d="M-1.2,-6 L1.2,-6 L1.5,-4.5 L2.8,-4 L4,-5 L5.5,-3.5 L4.5,-2.3 L5,-1 L6.5,-0.8 L6.5,1.2 L5,1.5 L4.5,2.8 L5.5,4 L4,5.5 L2.8,4.5 L1.5,5 L1.2,6.5 L-1.2,6.5 L-1.5,5 L-2.8,4.5 L-4,5.5 L-5.5,4 L-4.5,2.8 L-5,1.5 L-6.5,1.2 L-6.5,-0.8 L-5,-1 L-4.5,-2.3 L-5.5,-3.5 L-4,-5 L-2.8,-4 L-1.5,-4.5 Z"/><circle cx="0" cy="0" r="2.5" fill="#9575cd"/></g></g></svg>';
|
|
500
|
+
const venvDataUri = `data:image/svg+xml;base64,${btoa(venvSvg)}`;
|
|
497
501
|
|
|
498
502
|
// Inject CSS that overrides icons for .py and .md files
|
|
499
503
|
// Note: Jupytext marks .py and .md files as type="notebook", so we need to
|
|
@@ -698,6 +702,22 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
698
702
|
background-repeat: no-repeat;
|
|
699
703
|
background-position: center;
|
|
700
704
|
}
|
|
705
|
+
|
|
706
|
+
/* Override venv folder icons (.venv, venv, .env, env) */
|
|
707
|
+
.jp-DirListing-item[data-venv-folder] .jp-DirListing-itemIcon svg,
|
|
708
|
+
.jp-DirListing-item[data-venv-folder] .jp-DirListing-itemIcon img {
|
|
709
|
+
display: none !important;
|
|
710
|
+
}
|
|
711
|
+
.jp-DirListing-item[data-venv-folder] .jp-DirListing-itemIcon::before {
|
|
712
|
+
content: '';
|
|
713
|
+
display: inline-block;
|
|
714
|
+
width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
|
|
715
|
+
height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
|
|
716
|
+
background-image: url('${venvDataUri}');
|
|
717
|
+
background-size: contain;
|
|
718
|
+
background-repeat: no-repeat;
|
|
719
|
+
background-position: center;
|
|
720
|
+
}
|
|
701
721
|
`;
|
|
702
722
|
|
|
703
723
|
// Add CSS to make JavaScript and .env icons less bright
|
|
@@ -721,78 +741,72 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
721
741
|
}
|
|
722
742
|
`;
|
|
723
743
|
|
|
724
|
-
// Cache for Python package
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
// Folders to exclude from Python package checking
|
|
728
|
-
const excludedFolderPatterns = [
|
|
729
|
-
/^\.git$/,
|
|
730
|
-
/^\.hg$/,
|
|
731
|
-
/^\.svn$/,
|
|
732
|
-
/^\.venv$/,
|
|
733
|
-
/^venv$/,
|
|
734
|
-
/^\.env$/,
|
|
735
|
-
/^env$/,
|
|
736
|
-
/^node_modules$/,
|
|
737
|
-
/^__pycache__$/,
|
|
738
|
-
/^\.ipynb_checkpoints$/,
|
|
739
|
-
/^\.pytest_cache$/,
|
|
740
|
-
/^\.mypy_cache$/,
|
|
741
|
-
/^\.ruff_cache$/,
|
|
742
|
-
/^\.tox$/,
|
|
743
|
-
/^\.nox$/,
|
|
744
|
-
/^\.coverage$/,
|
|
745
|
-
/^htmlcov$/,
|
|
746
|
-
/^dist$/,
|
|
747
|
-
/^build$/,
|
|
748
|
-
/^\.eggs$/,
|
|
749
|
-
/\.egg-info$/,
|
|
750
|
-
/\.dist-info$/
|
|
751
|
-
];
|
|
752
|
-
|
|
753
|
-
const isExcludedFolder = (name: string): boolean => {
|
|
754
|
-
return excludedFolderPatterns.some(pattern => pattern.test(name));
|
|
755
|
-
};
|
|
744
|
+
// Cache for Python package detection (per directory)
|
|
745
|
+
let pythonPackagesCache: { path: string; packages: Set<string> } | null =
|
|
746
|
+
null;
|
|
756
747
|
|
|
757
|
-
//
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
folderName: string
|
|
761
|
-
): Promise<boolean> => {
|
|
762
|
-
// Skip excluded folders
|
|
763
|
-
if (isExcludedFolder(folderName)) {
|
|
764
|
-
return false;
|
|
765
|
-
}
|
|
748
|
+
// Detect Python packages by parsing pyproject.toml or setup.py
|
|
749
|
+
const detectPythonPackages = async (): Promise<Set<string>> => {
|
|
750
|
+
const currentPath = defaultFileBrowser?.model.path || '';
|
|
766
751
|
|
|
767
|
-
//
|
|
768
|
-
if (
|
|
769
|
-
return
|
|
752
|
+
// Return cached result if same directory
|
|
753
|
+
if (pythonPackagesCache?.path === currentPath) {
|
|
754
|
+
return pythonPackagesCache.packages;
|
|
770
755
|
}
|
|
771
|
-
const currentPath = defaultFileBrowser.model.path;
|
|
772
|
-
const fullPath = currentPath
|
|
773
|
-
? `${currentPath}/${folderName}`
|
|
774
|
-
: folderName;
|
|
775
756
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
757
|
+
const packages = new Set<string>();
|
|
758
|
+
|
|
779
759
|
try {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
760
|
+
const contents = await app.serviceManager.contents.get(currentPath, {
|
|
761
|
+
content: true
|
|
762
|
+
});
|
|
763
|
+
const files = contents.content || [];
|
|
764
|
+
|
|
765
|
+
// Check for pyproject.toml
|
|
766
|
+
const hasPyproject = files.some(
|
|
767
|
+
(f: any) => f.name === 'pyproject.toml'
|
|
768
|
+
);
|
|
769
|
+
if (hasPyproject) {
|
|
770
|
+
const pyprojectPath = currentPath
|
|
771
|
+
? `${currentPath}/pyproject.toml`
|
|
772
|
+
: 'pyproject.toml';
|
|
773
|
+
const file = await app.serviceManager.contents.get(pyprojectPath);
|
|
774
|
+
const content = file.content as string;
|
|
775
|
+
const parsed = parsePyprojectToml(content);
|
|
776
|
+
parsed.forEach(p => packages.add(p));
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Check for setup.py
|
|
780
|
+
const hasSetupPy = files.some((f: any) => f.name === 'setup.py');
|
|
781
|
+
if (hasSetupPy) {
|
|
782
|
+
const setupPath = currentPath
|
|
783
|
+
? `${currentPath}/setup.py`
|
|
784
|
+
: 'setup.py';
|
|
785
|
+
const file = await app.serviceManager.contents.get(setupPath);
|
|
786
|
+
const content = file.content as string;
|
|
787
|
+
const parsed = parseSetupPy(content);
|
|
788
|
+
parsed.forEach(p => packages.add(p));
|
|
789
|
+
}
|
|
788
790
|
} catch {
|
|
789
|
-
|
|
790
|
-
return false;
|
|
791
|
+
// Ignore errors - no packages detected
|
|
791
792
|
}
|
|
793
|
+
|
|
794
|
+
pythonPackagesCache = { path: currentPath, packages };
|
|
795
|
+
return packages;
|
|
792
796
|
};
|
|
793
797
|
|
|
798
|
+
// Invalidate cache on directory change
|
|
799
|
+
if (defaultFileBrowser) {
|
|
800
|
+
defaultFileBrowser.model.pathChanged.connect(() => {
|
|
801
|
+
pythonPackagesCache = null;
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
794
805
|
// Add a MutationObserver to mark special files in the file browser
|
|
795
|
-
const markSpecialFiles = () => {
|
|
806
|
+
const markSpecialFiles = async () => {
|
|
807
|
+
// Get Python packages for current directory (cached)
|
|
808
|
+
const pythonPackages = await detectPythonPackages();
|
|
809
|
+
|
|
796
810
|
// Process ALL items - clear wrong attributes and set correct ones
|
|
797
811
|
const allItems = document.querySelectorAll('.jp-DirListing-item');
|
|
798
812
|
allItems.forEach(item => {
|
|
@@ -890,24 +904,38 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
890
904
|
fileType === 'directory' ||
|
|
891
905
|
item.classList.contains('jp-DirListing-directory');
|
|
892
906
|
if (isDir) {
|
|
893
|
-
// Check if
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
907
|
+
// Check if folder is a venv folder (.venv, venv, .env, env)
|
|
908
|
+
const venvNames = ['.venv', 'venv', '.env', 'env'];
|
|
909
|
+
if (venvNames.includes(nameLower)) {
|
|
910
|
+
item.setAttribute('data-venv-folder', 'true');
|
|
911
|
+
item.removeAttribute('data-python-package');
|
|
912
|
+
} else {
|
|
913
|
+
item.removeAttribute('data-venv-folder');
|
|
914
|
+
// Check if folder name matches a detected Python package
|
|
915
|
+
if (pythonPackages.has(name)) {
|
|
897
916
|
item.setAttribute('data-python-package', 'true');
|
|
898
917
|
} else {
|
|
899
918
|
item.removeAttribute('data-python-package');
|
|
900
919
|
}
|
|
901
|
-
}
|
|
920
|
+
}
|
|
902
921
|
} else {
|
|
903
922
|
item.removeAttribute('data-python-package');
|
|
923
|
+
item.removeAttribute('data-venv-folder');
|
|
904
924
|
}
|
|
905
925
|
});
|
|
906
926
|
};
|
|
907
927
|
|
|
908
|
-
//
|
|
928
|
+
// Debounce timeout for MutationObserver
|
|
929
|
+
let markSpecialFilesTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
930
|
+
|
|
931
|
+
// Watch for changes in the file browser (debounced)
|
|
909
932
|
const observer = new MutationObserver(() => {
|
|
910
|
-
|
|
933
|
+
if (markSpecialFilesTimeout) {
|
|
934
|
+
clearTimeout(markSpecialFilesTimeout);
|
|
935
|
+
}
|
|
936
|
+
markSpecialFilesTimeout = setTimeout(() => {
|
|
937
|
+
markSpecialFiles();
|
|
938
|
+
}, 100);
|
|
911
939
|
});
|
|
912
940
|
|
|
913
941
|
// Start observing when the file browser is ready
|
package/src/parsers.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python package configuration file parsers
|
|
3
|
+
* Extracts package names from pyproject.toml and setup.py files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse pyproject.toml content to extract Python package names
|
|
8
|
+
* @param content - The content of pyproject.toml file
|
|
9
|
+
* @returns Set of package names found in the file
|
|
10
|
+
*/
|
|
11
|
+
export function parsePyprojectToml(content: string): Set<string> {
|
|
12
|
+
const packages = new Set<string>();
|
|
13
|
+
|
|
14
|
+
// Parse [project] name = "package_name"
|
|
15
|
+
const nameMatch = content.match(
|
|
16
|
+
/\[project\][^[]*name\s*=\s*["']([^"']+)["']/s
|
|
17
|
+
);
|
|
18
|
+
if (nameMatch) {
|
|
19
|
+
packages.add(nameMatch[1]);
|
|
20
|
+
// Also add underscore variant (package-name -> package_name)
|
|
21
|
+
packages.add(nameMatch[1].replace(/-/g, '_'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Parse packages = ["pkg1", "pkg2"]
|
|
25
|
+
const packagesMatch = content.match(/packages\s*=\s*\[([^\]]+)\]/);
|
|
26
|
+
if (packagesMatch) {
|
|
27
|
+
const pkgList = packagesMatch[1].match(/["']([^"']+)["']/g);
|
|
28
|
+
pkgList?.forEach(p => packages.add(p.replace(/["']/g, '')));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return packages;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse setup.py content to extract Python package names
|
|
36
|
+
* @param content - The content of setup.py file
|
|
37
|
+
* @returns Set of package names found in the file
|
|
38
|
+
*/
|
|
39
|
+
export function parseSetupPy(content: string): Set<string> {
|
|
40
|
+
const packages = new Set<string>();
|
|
41
|
+
|
|
42
|
+
// Parse packages=["pkg1", "pkg2"]
|
|
43
|
+
const packagesMatch = content.match(/packages\s*=\s*\[([^\]]+)\]/);
|
|
44
|
+
if (packagesMatch) {
|
|
45
|
+
const pkgList = packagesMatch[1].match(/["']([^"']+)["']/g);
|
|
46
|
+
pkgList?.forEach(p => packages.add(p.replace(/["']/g, '')));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Parse name="package_name"
|
|
50
|
+
const nameMatch = content.match(/name\s*=\s*["']([^"']+)["']/);
|
|
51
|
+
if (nameMatch) {
|
|
52
|
+
packages.add(nameMatch[1]);
|
|
53
|
+
packages.add(nameMatch[1].replace(/-/g, '_'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return packages;
|
|
57
|
+
}
|