jupyterlab_vscode_icons_extension 1.1.8 → 1.1.22
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 +146 -67
- 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 +171 -70
- 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
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
1
2
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
2
3
|
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
3
4
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
4
5
|
import { getIconSVG } from './icons';
|
|
6
|
+
import { parsePyprojectToml, parseSetupPy } from './parsers';
|
|
5
7
|
const PLUGIN_ID = 'jupyterlab_vscode_icons_extension:plugin';
|
|
6
8
|
/**
|
|
7
9
|
* Create a LabIcon from vscode-icons SVG data
|
|
@@ -127,7 +129,7 @@ const fileTypeConfigs = [
|
|
|
127
129
|
iconName: 'file-type-perl',
|
|
128
130
|
group: 'enableLanguageIcons'
|
|
129
131
|
},
|
|
130
|
-
// Shell scripts (.sh, .bash, .zsh) and batch files (.bat, .cmd) use custom icons with black backgrounds
|
|
132
|
+
// Shell scripts (.sh, .bash, .zsh, .fish, .csh, .nu) and batch files (.bat, .cmd) use custom icons with black backgrounds
|
|
131
133
|
// Registered separately below with custom SVGs
|
|
132
134
|
{
|
|
133
135
|
extensions: ['.ps1'],
|
|
@@ -442,6 +444,11 @@ const plugin = {
|
|
|
442
444
|
const pytestDataUri = `data:image/svg+xml;base64,${btoa(pytestSvg)}`;
|
|
443
445
|
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
446
|
const pythonPackageDataUri = `data:image/svg+xml;base64,${btoa(pythonPackageSvg)}`;
|
|
447
|
+
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>';
|
|
448
|
+
const venvDataUri = `data:image/svg+xml;base64,${btoa(venvSvg)}`;
|
|
449
|
+
// Executable file icon - JupyterLab standard file icon with play triangle overlay
|
|
450
|
+
const executableSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><path fill="#616161" d="m19.3 8.2-5.5-5.5c-.3-.3-.7-.5-1.2-.5H3.9c-.8.1-1.6.9-1.6 1.8v14.1c0 .9.7 1.6 1.6 1.6h14.2c.9 0 1.6-.7 1.6-1.6V9.4c.1-.5-.1-.9-.4-1.2m-5.8-3.3 3.4 3.6h-3.4zm3.9 12.7H4.7c-.1 0-.2 0-.2-.2V4.7c0-.2.1-.3.2-.3h7.2v4.4s0 .8.3 1.1 1.1.3 1.1.3h4.3v7.2s-.1.2-.2.2"/><path fill="#66bb6a" d="M12,12 L20,16 L12,20 Z"/></svg>';
|
|
451
|
+
const executableDataUri = `data:image/svg+xml;base64,${btoa(executableSvg)}`;
|
|
445
452
|
// Inject CSS that overrides icons for .py and .md files
|
|
446
453
|
// Note: Jupytext marks .py and .md files as type="notebook", so we need to
|
|
447
454
|
// use JavaScript to detect and mark these files for CSS targeting
|
|
@@ -645,6 +652,38 @@ const plugin = {
|
|
|
645
652
|
background-repeat: no-repeat;
|
|
646
653
|
background-position: center;
|
|
647
654
|
}
|
|
655
|
+
|
|
656
|
+
/* Override venv folder icons (.venv, venv, .env, env) */
|
|
657
|
+
.jp-DirListing-item[data-venv-folder] .jp-DirListing-itemIcon svg,
|
|
658
|
+
.jp-DirListing-item[data-venv-folder] .jp-DirListing-itemIcon img {
|
|
659
|
+
display: none !important;
|
|
660
|
+
}
|
|
661
|
+
.jp-DirListing-item[data-venv-folder] .jp-DirListing-itemIcon::before {
|
|
662
|
+
content: '';
|
|
663
|
+
display: inline-block;
|
|
664
|
+
width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
|
|
665
|
+
height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
|
|
666
|
+
background-image: url('${venvDataUri}');
|
|
667
|
+
background-size: contain;
|
|
668
|
+
background-repeat: no-repeat;
|
|
669
|
+
background-position: center;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/* Override executable file icons */
|
|
673
|
+
.jp-DirListing-item[data-executable] .jp-DirListing-itemIcon svg,
|
|
674
|
+
.jp-DirListing-item[data-executable] .jp-DirListing-itemIcon img {
|
|
675
|
+
display: none !important;
|
|
676
|
+
}
|
|
677
|
+
.jp-DirListing-item[data-executable] .jp-DirListing-itemIcon::before {
|
|
678
|
+
content: '';
|
|
679
|
+
display: inline-block;
|
|
680
|
+
width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
|
|
681
|
+
height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
|
|
682
|
+
background-image: url('${executableDataUri}');
|
|
683
|
+
background-size: contain;
|
|
684
|
+
background-repeat: no-repeat;
|
|
685
|
+
background-position: center;
|
|
686
|
+
}
|
|
648
687
|
`;
|
|
649
688
|
// Add CSS to make JavaScript and .env icons less bright
|
|
650
689
|
style.textContent += `
|
|
@@ -666,71 +705,91 @@ const plugin = {
|
|
|
666
705
|
opacity: 55% !important;
|
|
667
706
|
}
|
|
668
707
|
`;
|
|
669
|
-
// Cache for Python package
|
|
670
|
-
|
|
671
|
-
//
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
708
|
+
// Cache for Python package detection (per directory)
|
|
709
|
+
let pythonPackagesCache = null;
|
|
710
|
+
// Detect Python packages by parsing pyproject.toml or setup.py
|
|
711
|
+
const detectPythonPackages = async () => {
|
|
712
|
+
const currentPath = (defaultFileBrowser === null || defaultFileBrowser === void 0 ? void 0 : defaultFileBrowser.model.path) || '';
|
|
713
|
+
// Return cached result if same directory
|
|
714
|
+
if ((pythonPackagesCache === null || pythonPackagesCache === void 0 ? void 0 : pythonPackagesCache.path) === currentPath) {
|
|
715
|
+
return pythonPackagesCache.packages;
|
|
716
|
+
}
|
|
717
|
+
const packages = new Set();
|
|
718
|
+
try {
|
|
719
|
+
const contents = await app.serviceManager.contents.get(currentPath, {
|
|
720
|
+
content: true
|
|
721
|
+
});
|
|
722
|
+
const files = contents.content || [];
|
|
723
|
+
// Check for pyproject.toml
|
|
724
|
+
const hasPyproject = files.some((f) => f.name === 'pyproject.toml');
|
|
725
|
+
if (hasPyproject) {
|
|
726
|
+
const pyprojectPath = currentPath
|
|
727
|
+
? `${currentPath}/pyproject.toml`
|
|
728
|
+
: 'pyproject.toml';
|
|
729
|
+
const file = await app.serviceManager.contents.get(pyprojectPath);
|
|
730
|
+
const content = file.content;
|
|
731
|
+
const parsed = parsePyprojectToml(content);
|
|
732
|
+
parsed.forEach(p => packages.add(p));
|
|
733
|
+
}
|
|
734
|
+
// Check for setup.py
|
|
735
|
+
const hasSetupPy = files.some((f) => f.name === 'setup.py');
|
|
736
|
+
if (hasSetupPy) {
|
|
737
|
+
const setupPath = currentPath
|
|
738
|
+
? `${currentPath}/setup.py`
|
|
739
|
+
: 'setup.py';
|
|
740
|
+
const file = await app.serviceManager.contents.get(setupPath);
|
|
741
|
+
const content = file.content;
|
|
742
|
+
const parsed = parseSetupPy(content);
|
|
743
|
+
parsed.forEach(p => packages.add(p));
|
|
744
|
+
}
|
|
706
745
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
return false;
|
|
746
|
+
catch (_a) {
|
|
747
|
+
// Ignore errors - no packages detected
|
|
710
748
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
749
|
+
pythonPackagesCache = { path: currentPath, packages };
|
|
750
|
+
return packages;
|
|
751
|
+
};
|
|
752
|
+
// Cache for executable file detection (per directory)
|
|
753
|
+
let executablesCache = null;
|
|
754
|
+
// Detect executable files via server API
|
|
755
|
+
const detectExecutables = async () => {
|
|
756
|
+
if (!settings.enableExecutableIcons) {
|
|
757
|
+
return new Set();
|
|
717
758
|
}
|
|
759
|
+
const currentPath = (defaultFileBrowser === null || defaultFileBrowser === void 0 ? void 0 : defaultFileBrowser.model.path) || '';
|
|
760
|
+
// Return cached result if same directory
|
|
761
|
+
if ((executablesCache === null || executablesCache === void 0 ? void 0 : executablesCache.path) === currentPath) {
|
|
762
|
+
return executablesCache.executables;
|
|
763
|
+
}
|
|
764
|
+
const executables = new Set();
|
|
718
765
|
try {
|
|
719
|
-
|
|
720
|
-
const
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
766
|
+
const baseUrl = PageConfig.getBaseUrl();
|
|
767
|
+
const apiUrl = URLExt.join(baseUrl, 'vscode-icons', 'executables');
|
|
768
|
+
const response = await fetch(`${apiUrl}?path=${encodeURIComponent(currentPath)}`);
|
|
769
|
+
if (response.ok) {
|
|
770
|
+
const files = await response.json();
|
|
771
|
+
files.forEach(f => executables.add(f));
|
|
772
|
+
}
|
|
726
773
|
}
|
|
727
|
-
catch (
|
|
728
|
-
|
|
729
|
-
return false;
|
|
774
|
+
catch (_a) {
|
|
775
|
+
// Ignore errors - no executables detected
|
|
730
776
|
}
|
|
777
|
+
executablesCache = { path: currentPath, executables };
|
|
778
|
+
return executables;
|
|
731
779
|
};
|
|
780
|
+
// Invalidate cache on directory change
|
|
781
|
+
if (defaultFileBrowser) {
|
|
782
|
+
defaultFileBrowser.model.pathChanged.connect(() => {
|
|
783
|
+
pythonPackagesCache = null;
|
|
784
|
+
executablesCache = null;
|
|
785
|
+
});
|
|
786
|
+
}
|
|
732
787
|
// Add a MutationObserver to mark special files in the file browser
|
|
733
|
-
const markSpecialFiles = () => {
|
|
788
|
+
const markSpecialFiles = async () => {
|
|
789
|
+
// Get Python packages for current directory (cached)
|
|
790
|
+
const pythonPackages = await detectPythonPackages();
|
|
791
|
+
// Get executable files for current directory (cached, only if setting enabled)
|
|
792
|
+
const executables = await detectExecutables();
|
|
734
793
|
// Process ALL items - clear wrong attributes and set correct ones
|
|
735
794
|
const allItems = document.querySelectorAll('.jp-DirListing-item');
|
|
736
795
|
allItems.forEach(item => {
|
|
@@ -809,29 +868,48 @@ const plugin = {
|
|
|
809
868
|
nameLower === 'conftest.py') {
|
|
810
869
|
item.setAttribute('data-pytest', 'true');
|
|
811
870
|
}
|
|
871
|
+
// Mark executable files if setting is enabled (uses server API for +x detection)
|
|
872
|
+
item.removeAttribute('data-executable');
|
|
873
|
+
if (settings.enableExecutableIcons && executables.has(name)) {
|
|
874
|
+
item.setAttribute('data-executable', 'true');
|
|
875
|
+
}
|
|
812
876
|
// Check if this is a directory (folder)
|
|
813
877
|
const isDir = fileType === 'directory' ||
|
|
814
878
|
item.classList.contains('jp-DirListing-directory');
|
|
815
879
|
if (isDir) {
|
|
816
|
-
// Check if
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
880
|
+
// Check if folder is a venv folder (.venv, venv, .env, env)
|
|
881
|
+
const venvNames = ['.venv', 'venv', '.env', 'env'];
|
|
882
|
+
if (venvNames.includes(nameLower)) {
|
|
883
|
+
item.setAttribute('data-venv-folder', 'true');
|
|
884
|
+
item.removeAttribute('data-python-package');
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
item.removeAttribute('data-venv-folder');
|
|
888
|
+
// Check if folder name matches a detected Python package
|
|
889
|
+
if (pythonPackages.has(name)) {
|
|
820
890
|
item.setAttribute('data-python-package', 'true');
|
|
821
891
|
}
|
|
822
892
|
else {
|
|
823
893
|
item.removeAttribute('data-python-package');
|
|
824
894
|
}
|
|
825
|
-
}
|
|
895
|
+
}
|
|
826
896
|
}
|
|
827
897
|
else {
|
|
828
898
|
item.removeAttribute('data-python-package');
|
|
899
|
+
item.removeAttribute('data-venv-folder');
|
|
829
900
|
}
|
|
830
901
|
});
|
|
831
902
|
};
|
|
832
|
-
//
|
|
903
|
+
// Debounce timeout for MutationObserver
|
|
904
|
+
let markSpecialFilesTimeout = null;
|
|
905
|
+
// Watch for changes in the file browser (debounced)
|
|
833
906
|
const observer = new MutationObserver(() => {
|
|
834
|
-
|
|
907
|
+
if (markSpecialFilesTimeout) {
|
|
908
|
+
clearTimeout(markSpecialFilesTimeout);
|
|
909
|
+
}
|
|
910
|
+
markSpecialFilesTimeout = setTimeout(() => {
|
|
911
|
+
markSpecialFiles();
|
|
912
|
+
}, 100);
|
|
835
913
|
});
|
|
836
914
|
// Start observing when the file browser is ready
|
|
837
915
|
setTimeout(() => {
|
|
@@ -862,7 +940,8 @@ const plugin = {
|
|
|
862
940
|
enableDataIcons: true,
|
|
863
941
|
enableConfigIcons: true,
|
|
864
942
|
enableDocIcons: true,
|
|
865
|
-
enableImageIcons: true
|
|
943
|
+
enableImageIcons: true,
|
|
944
|
+
enableExecutableIcons: false
|
|
866
945
|
};
|
|
867
946
|
// Function to register file types based on settings
|
|
868
947
|
const registerFileTypes = () => {
|
|
@@ -1015,7 +1094,7 @@ const plugin = {
|
|
|
1015
1094
|
docRegistry.addFileType({
|
|
1016
1095
|
name: 'vscode-shell',
|
|
1017
1096
|
displayName: 'Shell Script',
|
|
1018
|
-
extensions: ['.sh', '.bash', '.zsh'],
|
|
1097
|
+
extensions: ['.sh', '.bash', '.zsh', '.fish', '.csh', '.nu'],
|
|
1019
1098
|
fileFormat: 'text',
|
|
1020
1099
|
contentType: 'file',
|
|
1021
1100
|
icon: shellIcon
|
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
|
@@ -2,10 +2,12 @@ import {
|
|
|
2
2
|
JupyterFrontEnd,
|
|
3
3
|
JupyterFrontEndPlugin
|
|
4
4
|
} from '@jupyterlab/application';
|
|
5
|
+
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
5
6
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
6
7
|
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
7
8
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
8
9
|
import { getIconSVG } from './icons';
|
|
10
|
+
import { parsePyprojectToml, parseSetupPy } from './parsers';
|
|
9
11
|
|
|
10
12
|
const PLUGIN_ID = 'jupyterlab_vscode_icons_extension:plugin';
|
|
11
13
|
|
|
@@ -17,6 +19,7 @@ interface IIconSettings {
|
|
|
17
19
|
enableConfigIcons: boolean;
|
|
18
20
|
enableDocIcons: boolean;
|
|
19
21
|
enableImageIcons: boolean;
|
|
22
|
+
enableExecutableIcons: boolean;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/**
|
|
@@ -156,7 +159,7 @@ const fileTypeConfigs: IFileTypeConfig[] = [
|
|
|
156
159
|
iconName: 'file-type-perl',
|
|
157
160
|
group: 'enableLanguageIcons'
|
|
158
161
|
},
|
|
159
|
-
// Shell scripts (.sh, .bash, .zsh) and batch files (.bat, .cmd) use custom icons with black backgrounds
|
|
162
|
+
// Shell scripts (.sh, .bash, .zsh, .fish, .csh, .nu) and batch files (.bat, .cmd) use custom icons with black backgrounds
|
|
160
163
|
// Registered separately below with custom SVGs
|
|
161
164
|
{
|
|
162
165
|
extensions: ['.ps1'],
|
|
@@ -494,6 +497,13 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
494
497
|
const pythonPackageSvg =
|
|
495
498
|
'<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
499
|
const pythonPackageDataUri = `data:image/svg+xml;base64,${btoa(pythonPackageSvg)}`;
|
|
500
|
+
const venvSvg =
|
|
501
|
+
'<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>';
|
|
502
|
+
const venvDataUri = `data:image/svg+xml;base64,${btoa(venvSvg)}`;
|
|
503
|
+
// Executable file icon - JupyterLab standard file icon with play triangle overlay
|
|
504
|
+
const executableSvg =
|
|
505
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><path fill="#616161" d="m19.3 8.2-5.5-5.5c-.3-.3-.7-.5-1.2-.5H3.9c-.8.1-1.6.9-1.6 1.8v14.1c0 .9.7 1.6 1.6 1.6h14.2c.9 0 1.6-.7 1.6-1.6V9.4c.1-.5-.1-.9-.4-1.2m-5.8-3.3 3.4 3.6h-3.4zm3.9 12.7H4.7c-.1 0-.2 0-.2-.2V4.7c0-.2.1-.3.2-.3h7.2v4.4s0 .8.3 1.1 1.1.3 1.1.3h4.3v7.2s-.1.2-.2.2"/><path fill="#66bb6a" d="M12,12 L20,16 L12,20 Z"/></svg>';
|
|
506
|
+
const executableDataUri = `data:image/svg+xml;base64,${btoa(executableSvg)}`;
|
|
497
507
|
|
|
498
508
|
// Inject CSS that overrides icons for .py and .md files
|
|
499
509
|
// Note: Jupytext marks .py and .md files as type="notebook", so we need to
|
|
@@ -698,6 +708,38 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
698
708
|
background-repeat: no-repeat;
|
|
699
709
|
background-position: center;
|
|
700
710
|
}
|
|
711
|
+
|
|
712
|
+
/* Override venv folder icons (.venv, venv, .env, env) */
|
|
713
|
+
.jp-DirListing-item[data-venv-folder] .jp-DirListing-itemIcon svg,
|
|
714
|
+
.jp-DirListing-item[data-venv-folder] .jp-DirListing-itemIcon img {
|
|
715
|
+
display: none !important;
|
|
716
|
+
}
|
|
717
|
+
.jp-DirListing-item[data-venv-folder] .jp-DirListing-itemIcon::before {
|
|
718
|
+
content: '';
|
|
719
|
+
display: inline-block;
|
|
720
|
+
width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
|
|
721
|
+
height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
|
|
722
|
+
background-image: url('${venvDataUri}');
|
|
723
|
+
background-size: contain;
|
|
724
|
+
background-repeat: no-repeat;
|
|
725
|
+
background-position: center;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/* Override executable file icons */
|
|
729
|
+
.jp-DirListing-item[data-executable] .jp-DirListing-itemIcon svg,
|
|
730
|
+
.jp-DirListing-item[data-executable] .jp-DirListing-itemIcon img {
|
|
731
|
+
display: none !important;
|
|
732
|
+
}
|
|
733
|
+
.jp-DirListing-item[data-executable] .jp-DirListing-itemIcon::before {
|
|
734
|
+
content: '';
|
|
735
|
+
display: inline-block;
|
|
736
|
+
width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
|
|
737
|
+
height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
|
|
738
|
+
background-image: url('${executableDataUri}');
|
|
739
|
+
background-size: contain;
|
|
740
|
+
background-repeat: no-repeat;
|
|
741
|
+
background-position: center;
|
|
742
|
+
}
|
|
701
743
|
`;
|
|
702
744
|
|
|
703
745
|
// Add CSS to make JavaScript and .env icons less bright
|
|
@@ -721,78 +763,116 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
721
763
|
}
|
|
722
764
|
`;
|
|
723
765
|
|
|
724
|
-
// Cache for Python package
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
};
|
|
766
|
+
// Cache for Python package detection (per directory)
|
|
767
|
+
let pythonPackagesCache: { path: string; packages: Set<string> } | null =
|
|
768
|
+
null;
|
|
769
|
+
|
|
770
|
+
// Detect Python packages by parsing pyproject.toml or setup.py
|
|
771
|
+
const detectPythonPackages = async (): Promise<Set<string>> => {
|
|
772
|
+
const currentPath = defaultFileBrowser?.model.path || '';
|
|
773
|
+
|
|
774
|
+
// Return cached result if same directory
|
|
775
|
+
if (pythonPackagesCache?.path === currentPath) {
|
|
776
|
+
return pythonPackagesCache.packages;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const packages = new Set<string>();
|
|
780
|
+
|
|
781
|
+
try {
|
|
782
|
+
const contents = await app.serviceManager.contents.get(currentPath, {
|
|
783
|
+
content: true
|
|
784
|
+
});
|
|
785
|
+
const files = contents.content || [];
|
|
756
786
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
787
|
+
// Check for pyproject.toml
|
|
788
|
+
const hasPyproject = files.some(
|
|
789
|
+
(f: any) => f.name === 'pyproject.toml'
|
|
790
|
+
);
|
|
791
|
+
if (hasPyproject) {
|
|
792
|
+
const pyprojectPath = currentPath
|
|
793
|
+
? `${currentPath}/pyproject.toml`
|
|
794
|
+
: 'pyproject.toml';
|
|
795
|
+
const file = await app.serviceManager.contents.get(pyprojectPath);
|
|
796
|
+
const content = file.content as string;
|
|
797
|
+
const parsed = parsePyprojectToml(content);
|
|
798
|
+
parsed.forEach(p => packages.add(p));
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Check for setup.py
|
|
802
|
+
const hasSetupPy = files.some((f: any) => f.name === 'setup.py');
|
|
803
|
+
if (hasSetupPy) {
|
|
804
|
+
const setupPath = currentPath
|
|
805
|
+
? `${currentPath}/setup.py`
|
|
806
|
+
: 'setup.py';
|
|
807
|
+
const file = await app.serviceManager.contents.get(setupPath);
|
|
808
|
+
const content = file.content as string;
|
|
809
|
+
const parsed = parseSetupPy(content);
|
|
810
|
+
parsed.forEach(p => packages.add(p));
|
|
811
|
+
}
|
|
812
|
+
} catch {
|
|
813
|
+
// Ignore errors - no packages detected
|
|
765
814
|
}
|
|
766
815
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
816
|
+
pythonPackagesCache = { path: currentPath, packages };
|
|
817
|
+
return packages;
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
// Cache for executable file detection (per directory)
|
|
821
|
+
let executablesCache: { path: string; executables: Set<string> } | null =
|
|
822
|
+
null;
|
|
823
|
+
|
|
824
|
+
// Detect executable files via server API
|
|
825
|
+
const detectExecutables = async (): Promise<Set<string>> => {
|
|
826
|
+
if (!settings.enableExecutableIcons) {
|
|
827
|
+
return new Set<string>();
|
|
770
828
|
}
|
|
771
|
-
const currentPath = defaultFileBrowser.model.path;
|
|
772
|
-
const fullPath = currentPath
|
|
773
|
-
? `${currentPath}/${folderName}`
|
|
774
|
-
: folderName;
|
|
775
829
|
|
|
776
|
-
|
|
777
|
-
|
|
830
|
+
const currentPath = defaultFileBrowser?.model.path || '';
|
|
831
|
+
|
|
832
|
+
// Return cached result if same directory
|
|
833
|
+
if (executablesCache?.path === currentPath) {
|
|
834
|
+
return executablesCache.executables;
|
|
778
835
|
}
|
|
836
|
+
|
|
837
|
+
const executables = new Set<string>();
|
|
838
|
+
|
|
779
839
|
try {
|
|
780
|
-
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
840
|
+
const baseUrl = PageConfig.getBaseUrl();
|
|
841
|
+
const apiUrl = URLExt.join(
|
|
842
|
+
baseUrl,
|
|
843
|
+
'vscode-icons',
|
|
844
|
+
'executables'
|
|
845
|
+
);
|
|
846
|
+
const response = await fetch(
|
|
847
|
+
`${apiUrl}?path=${encodeURIComponent(currentPath)}`
|
|
848
|
+
);
|
|
849
|
+
if (response.ok) {
|
|
850
|
+
const files: string[] = await response.json();
|
|
851
|
+
files.forEach(f => executables.add(f));
|
|
852
|
+
}
|
|
788
853
|
} catch {
|
|
789
|
-
|
|
790
|
-
return false;
|
|
854
|
+
// Ignore errors - no executables detected
|
|
791
855
|
}
|
|
856
|
+
|
|
857
|
+
executablesCache = { path: currentPath, executables };
|
|
858
|
+
return executables;
|
|
792
859
|
};
|
|
793
860
|
|
|
861
|
+
// Invalidate cache on directory change
|
|
862
|
+
if (defaultFileBrowser) {
|
|
863
|
+
defaultFileBrowser.model.pathChanged.connect(() => {
|
|
864
|
+
pythonPackagesCache = null;
|
|
865
|
+
executablesCache = null;
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
794
869
|
// Add a MutationObserver to mark special files in the file browser
|
|
795
|
-
const markSpecialFiles = () => {
|
|
870
|
+
const markSpecialFiles = async () => {
|
|
871
|
+
// Get Python packages for current directory (cached)
|
|
872
|
+
const pythonPackages = await detectPythonPackages();
|
|
873
|
+
// Get executable files for current directory (cached, only if setting enabled)
|
|
874
|
+
const executables = await detectExecutables();
|
|
875
|
+
|
|
796
876
|
// Process ALL items - clear wrong attributes and set correct ones
|
|
797
877
|
const allItems = document.querySelectorAll('.jp-DirListing-item');
|
|
798
878
|
allItems.forEach(item => {
|
|
@@ -885,29 +965,49 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
885
965
|
item.setAttribute('data-pytest', 'true');
|
|
886
966
|
}
|
|
887
967
|
|
|
968
|
+
// Mark executable files if setting is enabled (uses server API for +x detection)
|
|
969
|
+
item.removeAttribute('data-executable');
|
|
970
|
+
if (settings.enableExecutableIcons && executables.has(name)) {
|
|
971
|
+
item.setAttribute('data-executable', 'true');
|
|
972
|
+
}
|
|
973
|
+
|
|
888
974
|
// Check if this is a directory (folder)
|
|
889
975
|
const isDir =
|
|
890
976
|
fileType === 'directory' ||
|
|
891
977
|
item.classList.contains('jp-DirListing-directory');
|
|
892
978
|
if (isDir) {
|
|
893
|
-
// Check if
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
979
|
+
// Check if folder is a venv folder (.venv, venv, .env, env)
|
|
980
|
+
const venvNames = ['.venv', 'venv', '.env', 'env'];
|
|
981
|
+
if (venvNames.includes(nameLower)) {
|
|
982
|
+
item.setAttribute('data-venv-folder', 'true');
|
|
983
|
+
item.removeAttribute('data-python-package');
|
|
984
|
+
} else {
|
|
985
|
+
item.removeAttribute('data-venv-folder');
|
|
986
|
+
// Check if folder name matches a detected Python package
|
|
987
|
+
if (pythonPackages.has(name)) {
|
|
897
988
|
item.setAttribute('data-python-package', 'true');
|
|
898
989
|
} else {
|
|
899
990
|
item.removeAttribute('data-python-package');
|
|
900
991
|
}
|
|
901
|
-
}
|
|
992
|
+
}
|
|
902
993
|
} else {
|
|
903
994
|
item.removeAttribute('data-python-package');
|
|
995
|
+
item.removeAttribute('data-venv-folder');
|
|
904
996
|
}
|
|
905
997
|
});
|
|
906
998
|
};
|
|
907
999
|
|
|
908
|
-
//
|
|
1000
|
+
// Debounce timeout for MutationObserver
|
|
1001
|
+
let markSpecialFilesTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
1002
|
+
|
|
1003
|
+
// Watch for changes in the file browser (debounced)
|
|
909
1004
|
const observer = new MutationObserver(() => {
|
|
910
|
-
|
|
1005
|
+
if (markSpecialFilesTimeout) {
|
|
1006
|
+
clearTimeout(markSpecialFilesTimeout);
|
|
1007
|
+
}
|
|
1008
|
+
markSpecialFilesTimeout = setTimeout(() => {
|
|
1009
|
+
markSpecialFiles();
|
|
1010
|
+
}, 100);
|
|
911
1011
|
});
|
|
912
1012
|
|
|
913
1013
|
// Start observing when the file browser is ready
|
|
@@ -945,7 +1045,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
945
1045
|
enableDataIcons: true,
|
|
946
1046
|
enableConfigIcons: true,
|
|
947
1047
|
enableDocIcons: true,
|
|
948
|
-
enableImageIcons: true
|
|
1048
|
+
enableImageIcons: true,
|
|
1049
|
+
enableExecutableIcons: false
|
|
949
1050
|
};
|
|
950
1051
|
|
|
951
1052
|
// Function to register file types based on settings
|
|
@@ -1124,7 +1225,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
1124
1225
|
docRegistry.addFileType({
|
|
1125
1226
|
name: 'vscode-shell',
|
|
1126
1227
|
displayName: 'Shell Script',
|
|
1127
|
-
extensions: ['.sh', '.bash', '.zsh'],
|
|
1228
|
+
extensions: ['.sh', '.bash', '.zsh', '.fish', '.csh', '.nu'],
|
|
1128
1229
|
fileFormat: 'text',
|
|
1129
1230
|
contentType: 'file',
|
|
1130
1231
|
icon: shellIcon
|
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
|
+
}
|