jupyterlab_vscode_icons_extension 1.1.6 → 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 +94 -71
- 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 +103 -74
- 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
|
|
@@ -369,10 +370,10 @@ const plugin = {
|
|
|
369
370
|
const wordIcon = createLabIcon('file-type-word');
|
|
370
371
|
const excelIcon = createLabIcon('file-type-excel');
|
|
371
372
|
const powerpointIcon = createLabIcon('file-type-powerpoint');
|
|
372
|
-
// Custom Markdown icon (from markdown.svg - purple M with arrow,
|
|
373
|
+
// Custom Markdown icon (from markdown.svg - purple M with arrow, #8a2ea5)
|
|
373
374
|
const markdownSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 309 327">
|
|
374
|
-
<path fill="#
|
|
375
|
-
<path fill="#
|
|
375
|
+
<path fill="#8a2ea5" opacity="1" stroke="none" d="m 138.68393,230.48651 c 36.58836,3.1e-4 72.68422,3.1e-4 108.78008,3.1e-4 0.13988,0.49669 0.0821,0.12537 -3.34406,3.81472 -27.34165,24.16766 -54.43119,49.41695 -81.72391,73.62893 -2.65146,2.35216 -4.5582,3.21609 -7.64686,0.37229 -26.89754,-24.76539 -75.191307,-68.40096 -80.889724,-74.12425 -0.744118,-0.74735 -1.274501,-1.57204 -2.95867,-3.69233 23.309236,0 45.299954,0 67.783144,3.3e-4 z"/>
|
|
376
|
+
<path fill="#8a2ea5" d="m 61.156397,14.443673 h 69.176263 q 14.81059,56.661581 23.29958,97.452667 l 5.96036,-27.150338 q 3.61233,-15.870486 7.76652,-30.954008 l 10.6564,-39.348321 H 248.6367 L 276.09047,189.5437 H 221.90541 L 207.27544,69.137838 173.50009,189.5437 H 136.47364 L 101.07273,68.875516 86.984609,189.5437 H 35.147571 Z"/>
|
|
376
377
|
</svg>`;
|
|
377
378
|
// Custom PDF icon (document with folded corner and red PDF banner)
|
|
378
379
|
const pdfSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 16">
|
|
@@ -406,9 +407,10 @@ const plugin = {
|
|
|
406
407
|
<path fill="url(#py-b)" d="M55,0C29,0,29,10,29,12v13h27v4H19C11,29,0,34,0,55c0,20,8,27,16,27h9V69c0-6,3-16,16-16h26c4,0,15-2,15-14V14C82,11,82,0,55,0zM40,8c3,0,5,2,5,5s-2,5-5,5-5-2-5-5S37,8,40,8z"/>
|
|
407
408
|
<path fill="url(#py-y)" d="M55,110c26,0,26-10,26-12V85H54v-4h37c8,0,18-5,18-26 0-23-11-27-16-27h-9v13c0,6-3,16-16,16H42c-4,0-15,2-15,14v24c0,3,0,14,28,14zM70,101c-3,0-5-2-5-5s2-5,5-5 5,2,5,5S73,101,70,101z"/>
|
|
408
409
|
</svg>`;
|
|
409
|
-
// Custom README icon (info icon -
|
|
410
|
+
// Custom README icon (info icon - purple background #912bac, gray "i" #bdbdbd)
|
|
410
411
|
const readmeSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
411
|
-
<
|
|
412
|
+
<rect x="9.5" y="9.3" width="82" height="81.2" rx="4" fill="#912bac"/>
|
|
413
|
+
<path fill="#bdbdbd" d="m 45.736283,20.566722 h 11.01003 l 2.116667,2.116667 0,7.619586 -2.116667,2.116667 h -11.01003 l -2.116667,-2.116667 v -7.619586 z m -6.168542,19.478681 h 23.084596 l 2.116667,2.116667 v 5.700733 l -2.116597,2.09951 -3.175139,-0.02574 -2.116597,2.09951 V 67.32187 l 2.116667,2.116667 h 4.211629 l 2.116667,2.116667 v 5.567219 L 63.688967,79.23909 H 38.509408 l -2.116667,-2.116667 v -6.096386 l 2.116667,-2.116667 h 4.968955 L 45.59503,66.792703 V 52.096294 l -2.116667,-2.116667 h -3.910622 l -2.116667,-2.116667 0,-5.70089 z"/>
|
|
412
414
|
</svg>`;
|
|
413
415
|
// Get SVG content from VSCode icons
|
|
414
416
|
const claudeSvg = claudeIcon.svgstr;
|
|
@@ -441,6 +443,8 @@ const plugin = {
|
|
|
441
443
|
const pytestDataUri = `data:image/svg+xml;base64,${btoa(pytestSvg)}`;
|
|
442
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>';
|
|
443
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)}`;
|
|
444
448
|
// Inject CSS that overrides icons for .py and .md files
|
|
445
449
|
// Note: Jupytext marks .py and .md files as type="notebook", so we need to
|
|
446
450
|
// use JavaScript to detect and mark these files for CSS targeting
|
|
@@ -644,6 +648,22 @@ const plugin = {
|
|
|
644
648
|
background-repeat: no-repeat;
|
|
645
649
|
background-position: center;
|
|
646
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
|
+
}
|
|
647
667
|
`;
|
|
648
668
|
// Add CSS to make JavaScript and .env icons less bright
|
|
649
669
|
style.textContent += `
|
|
@@ -665,71 +685,60 @@ const plugin = {
|
|
|
665
685
|
opacity: 55% !important;
|
|
666
686
|
}
|
|
667
687
|
`;
|
|
668
|
-
// Cache for Python package
|
|
669
|
-
|
|
670
|
-
//
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
/^venv$/,
|
|
677
|
-
/^\.env$/,
|
|
678
|
-
/^env$/,
|
|
679
|
-
/^node_modules$/,
|
|
680
|
-
/^__pycache__$/,
|
|
681
|
-
/^\.ipynb_checkpoints$/,
|
|
682
|
-
/^\.pytest_cache$/,
|
|
683
|
-
/^\.mypy_cache$/,
|
|
684
|
-
/^\.ruff_cache$/,
|
|
685
|
-
/^\.tox$/,
|
|
686
|
-
/^\.nox$/,
|
|
687
|
-
/^\.coverage$/,
|
|
688
|
-
/^htmlcov$/,
|
|
689
|
-
/^dist$/,
|
|
690
|
-
/^build$/,
|
|
691
|
-
/^\.eggs$/,
|
|
692
|
-
/\.egg-info$/,
|
|
693
|
-
/\.dist-info$/
|
|
694
|
-
];
|
|
695
|
-
const isExcludedFolder = (name) => {
|
|
696
|
-
return excludedFolderPatterns.some(pattern => pattern.test(name));
|
|
697
|
-
};
|
|
698
|
-
// Check if a folder is a Python package (contains __init__.py)
|
|
699
|
-
// Use the file browser's model to get the correct path
|
|
700
|
-
const checkPythonPackage = async (folderName) => {
|
|
701
|
-
var _a;
|
|
702
|
-
// Skip excluded folders
|
|
703
|
-
if (isExcludedFolder(folderName)) {
|
|
704
|
-
return false;
|
|
705
|
-
}
|
|
706
|
-
// Get the current path from the file browser model
|
|
707
|
-
if (!defaultFileBrowser) {
|
|
708
|
-
return false;
|
|
709
|
-
}
|
|
710
|
-
const currentPath = defaultFileBrowser.model.path;
|
|
711
|
-
const fullPath = currentPath
|
|
712
|
-
? `${currentPath}/${folderName}`
|
|
713
|
-
: folderName;
|
|
714
|
-
if (pythonPackageCache.has(fullPath)) {
|
|
715
|
-
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;
|
|
716
696
|
}
|
|
697
|
+
const packages = new Set();
|
|
717
698
|
try {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
+
}
|
|
725
725
|
}
|
|
726
|
-
catch (
|
|
727
|
-
|
|
728
|
-
return false;
|
|
726
|
+
catch (_a) {
|
|
727
|
+
// Ignore errors - no packages detected
|
|
729
728
|
}
|
|
729
|
+
pythonPackagesCache = { path: currentPath, packages };
|
|
730
|
+
return packages;
|
|
730
731
|
};
|
|
732
|
+
// Invalidate cache on directory change
|
|
733
|
+
if (defaultFileBrowser) {
|
|
734
|
+
defaultFileBrowser.model.pathChanged.connect(() => {
|
|
735
|
+
pythonPackagesCache = null;
|
|
736
|
+
});
|
|
737
|
+
}
|
|
731
738
|
// Add a MutationObserver to mark special files in the file browser
|
|
732
|
-
const markSpecialFiles = () => {
|
|
739
|
+
const markSpecialFiles = async () => {
|
|
740
|
+
// Get Python packages for current directory (cached)
|
|
741
|
+
const pythonPackages = await detectPythonPackages();
|
|
733
742
|
// Process ALL items - clear wrong attributes and set correct ones
|
|
734
743
|
const allItems = document.querySelectorAll('.jp-DirListing-item');
|
|
735
744
|
allItems.forEach(item => {
|
|
@@ -812,25 +821,39 @@ const plugin = {
|
|
|
812
821
|
const isDir = fileType === 'directory' ||
|
|
813
822
|
item.classList.contains('jp-DirListing-directory');
|
|
814
823
|
if (isDir) {
|
|
815
|
-
// Check if
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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)) {
|
|
819
834
|
item.setAttribute('data-python-package', 'true');
|
|
820
835
|
}
|
|
821
836
|
else {
|
|
822
837
|
item.removeAttribute('data-python-package');
|
|
823
838
|
}
|
|
824
|
-
}
|
|
839
|
+
}
|
|
825
840
|
}
|
|
826
841
|
else {
|
|
827
842
|
item.removeAttribute('data-python-package');
|
|
843
|
+
item.removeAttribute('data-venv-folder');
|
|
828
844
|
}
|
|
829
845
|
});
|
|
830
846
|
};
|
|
831
|
-
//
|
|
847
|
+
// Debounce timeout for MutationObserver
|
|
848
|
+
let markSpecialFilesTimeout = null;
|
|
849
|
+
// Watch for changes in the file browser (debounced)
|
|
832
850
|
const observer = new MutationObserver(() => {
|
|
833
|
-
|
|
851
|
+
if (markSpecialFilesTimeout) {
|
|
852
|
+
clearTimeout(markSpecialFilesTimeout);
|
|
853
|
+
}
|
|
854
|
+
markSpecialFilesTimeout = setTimeout(() => {
|
|
855
|
+
markSpecialFiles();
|
|
856
|
+
}, 100);
|
|
834
857
|
});
|
|
835
858
|
// Start observing when the file browser is ready
|
|
836
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
|
|
|
@@ -413,10 +414,10 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
413
414
|
const excelIcon = createLabIcon('file-type-excel');
|
|
414
415
|
const powerpointIcon = createLabIcon('file-type-powerpoint');
|
|
415
416
|
|
|
416
|
-
// Custom Markdown icon (from markdown.svg - purple M with arrow,
|
|
417
|
+
// Custom Markdown icon (from markdown.svg - purple M with arrow, #8a2ea5)
|
|
417
418
|
const markdownSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 309 327">
|
|
418
|
-
<path fill="#
|
|
419
|
-
<path fill="#
|
|
419
|
+
<path fill="#8a2ea5" opacity="1" stroke="none" d="m 138.68393,230.48651 c 36.58836,3.1e-4 72.68422,3.1e-4 108.78008,3.1e-4 0.13988,0.49669 0.0821,0.12537 -3.34406,3.81472 -27.34165,24.16766 -54.43119,49.41695 -81.72391,73.62893 -2.65146,2.35216 -4.5582,3.21609 -7.64686,0.37229 -26.89754,-24.76539 -75.191307,-68.40096 -80.889724,-74.12425 -0.744118,-0.74735 -1.274501,-1.57204 -2.95867,-3.69233 23.309236,0 45.299954,0 67.783144,3.3e-4 z"/>
|
|
420
|
+
<path fill="#8a2ea5" d="m 61.156397,14.443673 h 69.176263 q 14.81059,56.661581 23.29958,97.452667 l 5.96036,-27.150338 q 3.61233,-15.870486 7.76652,-30.954008 l 10.6564,-39.348321 H 248.6367 L 276.09047,189.5437 H 221.90541 L 207.27544,69.137838 173.50009,189.5437 H 136.47364 L 101.07273,68.875516 86.984609,189.5437 H 35.147571 Z"/>
|
|
420
421
|
</svg>`;
|
|
421
422
|
|
|
422
423
|
// Custom PDF icon (document with folded corner and red PDF banner)
|
|
@@ -453,9 +454,10 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
453
454
|
<path fill="url(#py-y)" d="M55,110c26,0,26-10,26-12V85H54v-4h37c8,0,18-5,18-26 0-23-11-27-16-27h-9v13c0,6-3,16-16,16H42c-4,0-15,2-15,14v24c0,3,0,14,28,14zM70,101c-3,0-5-2-5-5s2-5,5-5 5,2,5,5S73,101,70,101z"/>
|
|
454
455
|
</svg>`;
|
|
455
456
|
|
|
456
|
-
// Custom README icon (info icon -
|
|
457
|
+
// Custom README icon (info icon - purple background #912bac, gray "i" #bdbdbd)
|
|
457
458
|
const readmeSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
458
|
-
<
|
|
459
|
+
<rect x="9.5" y="9.3" width="82" height="81.2" rx="4" fill="#912bac"/>
|
|
460
|
+
<path fill="#bdbdbd" d="m 45.736283,20.566722 h 11.01003 l 2.116667,2.116667 0,7.619586 -2.116667,2.116667 h -11.01003 l -2.116667,-2.116667 v -7.619586 z m -6.168542,19.478681 h 23.084596 l 2.116667,2.116667 v 5.700733 l -2.116597,2.09951 -3.175139,-0.02574 -2.116597,2.09951 V 67.32187 l 2.116667,2.116667 h 4.211629 l 2.116667,2.116667 v 5.567219 L 63.688967,79.23909 H 38.509408 l -2.116667,-2.116667 v -6.096386 l 2.116667,-2.116667 h 4.968955 L 45.59503,66.792703 V 52.096294 l -2.116667,-2.116667 h -3.910622 l -2.116667,-2.116667 0,-5.70089 z"/>
|
|
459
461
|
</svg>`;
|
|
460
462
|
|
|
461
463
|
// Get SVG content from VSCode icons
|
|
@@ -493,6 +495,9 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
493
495
|
const pythonPackageSvg =
|
|
494
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>';
|
|
495
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)}`;
|
|
496
501
|
|
|
497
502
|
// Inject CSS that overrides icons for .py and .md files
|
|
498
503
|
// Note: Jupytext marks .py and .md files as type="notebook", so we need to
|
|
@@ -697,6 +702,22 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
697
702
|
background-repeat: no-repeat;
|
|
698
703
|
background-position: center;
|
|
699
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
|
+
}
|
|
700
721
|
`;
|
|
701
722
|
|
|
702
723
|
// Add CSS to make JavaScript and .env icons less bright
|
|
@@ -720,78 +741,72 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
720
741
|
}
|
|
721
742
|
`;
|
|
722
743
|
|
|
723
|
-
// Cache for Python package
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
// Folders to exclude from Python package checking
|
|
727
|
-
const excludedFolderPatterns = [
|
|
728
|
-
/^\.git$/,
|
|
729
|
-
/^\.hg$/,
|
|
730
|
-
/^\.svn$/,
|
|
731
|
-
/^\.venv$/,
|
|
732
|
-
/^venv$/,
|
|
733
|
-
/^\.env$/,
|
|
734
|
-
/^env$/,
|
|
735
|
-
/^node_modules$/,
|
|
736
|
-
/^__pycache__$/,
|
|
737
|
-
/^\.ipynb_checkpoints$/,
|
|
738
|
-
/^\.pytest_cache$/,
|
|
739
|
-
/^\.mypy_cache$/,
|
|
740
|
-
/^\.ruff_cache$/,
|
|
741
|
-
/^\.tox$/,
|
|
742
|
-
/^\.nox$/,
|
|
743
|
-
/^\.coverage$/,
|
|
744
|
-
/^htmlcov$/,
|
|
745
|
-
/^dist$/,
|
|
746
|
-
/^build$/,
|
|
747
|
-
/^\.eggs$/,
|
|
748
|
-
/\.egg-info$/,
|
|
749
|
-
/\.dist-info$/
|
|
750
|
-
];
|
|
751
|
-
|
|
752
|
-
const isExcludedFolder = (name: string): boolean => {
|
|
753
|
-
return excludedFolderPatterns.some(pattern => pattern.test(name));
|
|
754
|
-
};
|
|
744
|
+
// Cache for Python package detection (per directory)
|
|
745
|
+
let pythonPackagesCache: { path: string; packages: Set<string> } | null =
|
|
746
|
+
null;
|
|
755
747
|
|
|
756
|
-
//
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
folderName: string
|
|
760
|
-
): Promise<boolean> => {
|
|
761
|
-
// Skip excluded folders
|
|
762
|
-
if (isExcludedFolder(folderName)) {
|
|
763
|
-
return false;
|
|
764
|
-
}
|
|
748
|
+
// Detect Python packages by parsing pyproject.toml or setup.py
|
|
749
|
+
const detectPythonPackages = async (): Promise<Set<string>> => {
|
|
750
|
+
const currentPath = defaultFileBrowser?.model.path || '';
|
|
765
751
|
|
|
766
|
-
//
|
|
767
|
-
if (
|
|
768
|
-
return
|
|
752
|
+
// Return cached result if same directory
|
|
753
|
+
if (pythonPackagesCache?.path === currentPath) {
|
|
754
|
+
return pythonPackagesCache.packages;
|
|
769
755
|
}
|
|
770
|
-
const currentPath = defaultFileBrowser.model.path;
|
|
771
|
-
const fullPath = currentPath
|
|
772
|
-
? `${currentPath}/${folderName}`
|
|
773
|
-
: folderName;
|
|
774
756
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
757
|
+
const packages = new Set<string>();
|
|
758
|
+
|
|
778
759
|
try {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
+
}
|
|
787
790
|
} catch {
|
|
788
|
-
|
|
789
|
-
return false;
|
|
791
|
+
// Ignore errors - no packages detected
|
|
790
792
|
}
|
|
793
|
+
|
|
794
|
+
pythonPackagesCache = { path: currentPath, packages };
|
|
795
|
+
return packages;
|
|
791
796
|
};
|
|
792
797
|
|
|
798
|
+
// Invalidate cache on directory change
|
|
799
|
+
if (defaultFileBrowser) {
|
|
800
|
+
defaultFileBrowser.model.pathChanged.connect(() => {
|
|
801
|
+
pythonPackagesCache = null;
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
793
805
|
// Add a MutationObserver to mark special files in the file browser
|
|
794
|
-
const markSpecialFiles = () => {
|
|
806
|
+
const markSpecialFiles = async () => {
|
|
807
|
+
// Get Python packages for current directory (cached)
|
|
808
|
+
const pythonPackages = await detectPythonPackages();
|
|
809
|
+
|
|
795
810
|
// Process ALL items - clear wrong attributes and set correct ones
|
|
796
811
|
const allItems = document.querySelectorAll('.jp-DirListing-item');
|
|
797
812
|
allItems.forEach(item => {
|
|
@@ -889,24 +904,38 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
889
904
|
fileType === 'directory' ||
|
|
890
905
|
item.classList.contains('jp-DirListing-directory');
|
|
891
906
|
if (isDir) {
|
|
892
|
-
// Check if
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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)) {
|
|
896
916
|
item.setAttribute('data-python-package', 'true');
|
|
897
917
|
} else {
|
|
898
918
|
item.removeAttribute('data-python-package');
|
|
899
919
|
}
|
|
900
|
-
}
|
|
920
|
+
}
|
|
901
921
|
} else {
|
|
902
922
|
item.removeAttribute('data-python-package');
|
|
923
|
+
item.removeAttribute('data-venv-folder');
|
|
903
924
|
}
|
|
904
925
|
});
|
|
905
926
|
};
|
|
906
927
|
|
|
907
|
-
//
|
|
928
|
+
// Debounce timeout for MutationObserver
|
|
929
|
+
let markSpecialFilesTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
930
|
+
|
|
931
|
+
// Watch for changes in the file browser (debounced)
|
|
908
932
|
const observer = new MutationObserver(() => {
|
|
909
|
-
|
|
933
|
+
if (markSpecialFilesTimeout) {
|
|
934
|
+
clearTimeout(markSpecialFilesTimeout);
|
|
935
|
+
}
|
|
936
|
+
markSpecialFilesTimeout = setTimeout(() => {
|
|
937
|
+
markSpecialFiles();
|
|
938
|
+
}, 100);
|
|
910
939
|
});
|
|
911
940
|
|
|
912
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
|
+
}
|