jupyterlab_vscode_icons_extension 1.0.117 → 1.1.8

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/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
2
+ import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
2
3
  import { LabIcon } from '@jupyterlab/ui-components';
3
4
  import { getIconSVG } from './icons';
4
5
  const PLUGIN_ID = 'jupyterlab_vscode_icons_extension:plugin';
@@ -358,8 +359,8 @@ const plugin = {
358
359
  id: PLUGIN_ID,
359
360
  description: 'Jupyterlab extension with a shameless rip-off of the vscode-icons into our beloved environment',
360
361
  autoStart: true,
361
- optional: [ISettingRegistry],
362
- activate: (app, settingRegistry) => {
362
+ optional: [ISettingRegistry, IDefaultFileBrowser],
363
+ activate: (app, settingRegistry, defaultFileBrowser) => {
363
364
  const { docRegistry } = app;
364
365
  // Function to inject CSS that overrides Jupytext icons
365
366
  const injectIconOverrideCSS = () => {
@@ -368,10 +369,10 @@ const plugin = {
368
369
  const wordIcon = createLabIcon('file-type-word');
369
370
  const excelIcon = createLabIcon('file-type-excel');
370
371
  const powerpointIcon = createLabIcon('file-type-powerpoint');
371
- // Custom Markdown icon (from markdown.svg - purple M with arrow, darker #7a2491)
372
+ // Custom Markdown icon (from markdown.svg - purple M with arrow, #8a2ea5)
372
373
  const markdownSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 309 327">
373
- <path fill="#7a2491" 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"/>
374
- <path fill="#7a2491" 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"/>
374
+ <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"/>
375
+ <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"/>
375
376
  </svg>`;
376
377
  // Custom PDF icon (document with folded corner and red PDF banner)
377
378
  const pdfSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 16">
@@ -405,9 +406,10 @@ const plugin = {
405
406
  <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"/>
406
407
  <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"/>
407
408
  </svg>`;
408
- // Custom README icon (info icon - centered, color #912bac from info2.svg)
409
+ // Custom README icon (info icon - purple background #912bac, gray "i" #bdbdbd)
409
410
  const readmeSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
410
- <path fill="#912bac" d="m 9.5247234,11.42511 v 76.943619 l 2.1166666,2.116667 h 77.809717 l 2.116667,-2.116667 v -76.94362 l -2.116667,-2.1166662 -77.809717,0 z m 36.2115596,9.141612 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"/>
411
+ <rect x="9.5" y="9.3" width="82" height="81.2" rx="4" fill="#912bac"/>
412
+ <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"/>
411
413
  </svg>`;
412
414
  // Get SVG content from VSCode icons
413
415
  const claudeSvg = claudeIcon.svgstr;
@@ -422,10 +424,24 @@ const plugin = {
422
424
  const claudeDataUri = `data:image/svg+xml;base64,${btoa(claudeSvg)}`;
423
425
  const readmeDataUri = `data:image/svg+xml;base64,${btoa(readmeSvg)}`;
424
426
  const pdfDataUri = `data:image/svg+xml;base64,${btoa(pdfSvg)}`;
425
- const wordDataUri = wordSvg ? `data:image/svg+xml;base64,${btoa(wordSvg)}` : '';
426
- const excelDataUri = excelSvg ? `data:image/svg+xml;base64,${btoa(excelSvg)}` : '';
427
- const powerpointDataUri = powerpointSvg ? `data:image/svg+xml;base64,${btoa(powerpointSvg)}` : '';
428
- const svgFileDataUri = svgFileSvg ? `data:image/svg+xml;base64,${btoa(svgFileSvg)}` : '';
427
+ const wordDataUri = wordSvg
428
+ ? `data:image/svg+xml;base64,${btoa(wordSvg)}`
429
+ : '';
430
+ const excelDataUri = excelSvg
431
+ ? `data:image/svg+xml;base64,${btoa(excelSvg)}`
432
+ : '';
433
+ const powerpointDataUri = powerpointSvg
434
+ ? `data:image/svg+xml;base64,${btoa(powerpointSvg)}`
435
+ : '';
436
+ const svgFileDataUri = svgFileSvg
437
+ ? `data:image/svg+xml;base64,${btoa(svgFileSvg)}`
438
+ : '';
439
+ const uvSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 330 330"><rect height="100%" width="100%" rx="66" fill="#26102f"/><path fill="#d256dc" d="M 65,65 h92 v130 h16 v-130 h92 v200 h-16 v-20 h-8 a20,20 0 0 1 -20,20 h-136 a20,20 0 0 1 -20,-20 z"/></svg>';
440
+ const uvDataUri = `data:image/svg+xml;base64,${btoa(uvSvg)}`;
441
+ const pytestSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 474 542"><path fill="#696969" d="M21,43h431c12,0 21,9 21,21c0,12-9,21-21,21H21c-12,0-21-9-21-21c0-12,9-21,21-21z"/><path fill="#009fe3" d="M25,0h87v20H25z"/><path fill="#c7d302" d="M138,0h87v20h-87z"/><path fill="#f07e16" d="M250,0h87v20h-87z"/><path fill="#df2815" d="M362,0h87v20h-87z"/><path fill="#df2815" d="M362,107h87v147h-87z"/><path fill="#f07e16" d="M250,107h87v238h-87z"/><path fill="#c7d302" d="M138,107h87v357h-87z"/><path fill="#009fe3" d="M25,107h87v435h-87z"/></svg>';
442
+ const pytestDataUri = `data:image/svg+xml;base64,${btoa(pytestSvg)}`;
443
+ 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
+ const pythonPackageDataUri = `data:image/svg+xml;base64,${btoa(pythonPackageSvg)}`;
429
445
  // Inject CSS that overrides icons for .py and .md files
430
446
  // Note: Jupytext marks .py and .md files as type="notebook", so we need to
431
447
  // use JavaScript to detect and mark these files for CSS targeting
@@ -581,6 +597,54 @@ const plugin = {
581
597
  background-repeat: no-repeat;
582
598
  background-position: center;
583
599
  }
600
+
601
+ /* Override uv.lock file icon with UV icon */
602
+ .jp-DirListing-item[data-uv-lock] .jp-DirListing-itemIcon svg,
603
+ .jp-DirListing-item[data-uv-lock] .jp-DirListing-itemIcon img {
604
+ display: none !important;
605
+ }
606
+ .jp-DirListing-item[data-uv-lock] .jp-DirListing-itemIcon::before {
607
+ content: '';
608
+ display: inline-block;
609
+ width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
610
+ height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
611
+ background-image: url('${uvDataUri}');
612
+ background-size: contain;
613
+ background-repeat: no-repeat;
614
+ background-position: center;
615
+ }
616
+
617
+ /* Override pytest-related file icons */
618
+ .jp-DirListing-item[data-pytest] .jp-DirListing-itemIcon svg,
619
+ .jp-DirListing-item[data-pytest] .jp-DirListing-itemIcon img {
620
+ display: none !important;
621
+ }
622
+ .jp-DirListing-item[data-pytest] .jp-DirListing-itemIcon::before {
623
+ content: '';
624
+ display: inline-block;
625
+ width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
626
+ height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
627
+ background-image: url('${pytestDataUri}');
628
+ background-size: contain;
629
+ background-repeat: no-repeat;
630
+ background-position: center;
631
+ }
632
+
633
+ /* Override Python package folder icons */
634
+ .jp-DirListing-item[data-python-package] .jp-DirListing-itemIcon svg,
635
+ .jp-DirListing-item[data-python-package] .jp-DirListing-itemIcon img {
636
+ display: none !important;
637
+ }
638
+ .jp-DirListing-item[data-python-package] .jp-DirListing-itemIcon::before {
639
+ content: '';
640
+ display: inline-block;
641
+ width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
642
+ height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
643
+ background-image: url('${pythonPackageDataUri}');
644
+ background-size: contain;
645
+ background-repeat: no-repeat;
646
+ background-position: center;
647
+ }
584
648
  `;
585
649
  // Add CSS to make JavaScript and .env icons less bright
586
650
  style.textContent += `
@@ -602,6 +666,69 @@ const plugin = {
602
666
  opacity: 55% !important;
603
667
  }
604
668
  `;
669
+ // Cache for Python package folder checks
670
+ const pythonPackageCache = new Map();
671
+ // Folders to exclude from Python package checking
672
+ const excludedFolderPatterns = [
673
+ /^\.git$/,
674
+ /^\.hg$/,
675
+ /^\.svn$/,
676
+ /^\.venv$/,
677
+ /^venv$/,
678
+ /^\.env$/,
679
+ /^env$/,
680
+ /^node_modules$/,
681
+ /^__pycache__$/,
682
+ /^\.ipynb_checkpoints$/,
683
+ /^\.pytest_cache$/,
684
+ /^\.mypy_cache$/,
685
+ /^\.ruff_cache$/,
686
+ /^\.tox$/,
687
+ /^\.nox$/,
688
+ /^\.coverage$/,
689
+ /^htmlcov$/,
690
+ /^dist$/,
691
+ /^build$/,
692
+ /^\.eggs$/,
693
+ /\.egg-info$/,
694
+ /\.dist-info$/
695
+ ];
696
+ const isExcludedFolder = (name) => {
697
+ return excludedFolderPatterns.some(pattern => pattern.test(name));
698
+ };
699
+ // Check if a folder is a Python package (contains __init__.py)
700
+ // Use the file browser's model to get the correct path
701
+ const checkPythonPackage = async (folderName) => {
702
+ var _a;
703
+ // Skip excluded folders
704
+ if (isExcludedFolder(folderName)) {
705
+ return false;
706
+ }
707
+ // Get the current path from the file browser model
708
+ if (!defaultFileBrowser) {
709
+ return false;
710
+ }
711
+ const currentPath = defaultFileBrowser.model.path;
712
+ const fullPath = currentPath
713
+ ? `${currentPath}/${folderName}`
714
+ : folderName;
715
+ if (pythonPackageCache.has(fullPath)) {
716
+ return pythonPackageCache.get(fullPath);
717
+ }
718
+ try {
719
+ // Use JupyterLab's contents manager with the correct path from file browser
720
+ const contents = app.serviceManager.contents;
721
+ const model = await contents.get(fullPath, { content: true });
722
+ const hasInit = ((_a = model.content) === null || _a === void 0 ? void 0 : _a.some((item) => item.name === '__init__.py')) ||
723
+ false;
724
+ pythonPackageCache.set(fullPath, hasInit);
725
+ return hasInit;
726
+ }
727
+ catch (_b) {
728
+ pythonPackageCache.set(fullPath, false);
729
+ return false;
730
+ }
731
+ };
605
732
  // Add a MutationObserver to mark special files in the file browser
606
733
  const markSpecialFiles = () => {
607
734
  // Process ALL items - clear wrong attributes and set correct ones
@@ -652,13 +779,17 @@ const plugin = {
652
779
  if (nameLower.endsWith('.pdf')) {
653
780
  item.setAttribute('data-vscode-pdf', 'true');
654
781
  }
655
- else if (nameLower.endsWith('.doc') || nameLower.endsWith('.docx')) {
782
+ else if (nameLower.endsWith('.doc') ||
783
+ nameLower.endsWith('.docx')) {
656
784
  item.setAttribute('data-vscode-word', 'true');
657
785
  }
658
- else if (nameLower.endsWith('.xls') || nameLower.endsWith('.xlsx') || nameLower.endsWith('.xlsm')) {
786
+ else if (nameLower.endsWith('.xls') ||
787
+ nameLower.endsWith('.xlsx') ||
788
+ nameLower.endsWith('.xlsm')) {
659
789
  item.setAttribute('data-vscode-excel', 'true');
660
790
  }
661
- else if (nameLower.endsWith('.ppt') || nameLower.endsWith('.pptx')) {
791
+ else if (nameLower.endsWith('.ppt') ||
792
+ nameLower.endsWith('.pptx')) {
662
793
  item.setAttribute('data-vscode-powerpoint', 'true');
663
794
  }
664
795
  // Force SVG icon for .svg files (override any incorrect file type detection)
@@ -666,6 +797,36 @@ const plugin = {
666
797
  if (nameLower.endsWith('.svg')) {
667
798
  item.setAttribute('data-vscode-svg-override', 'true');
668
799
  }
800
+ // Force UV icon for uv.lock file
801
+ item.removeAttribute('data-uv-lock');
802
+ if (nameLower === 'uv.lock') {
803
+ item.setAttribute('data-uv-lock', 'true');
804
+ }
805
+ // Force pytest icon for pytest-related files
806
+ item.removeAttribute('data-pytest');
807
+ if (nameLower === '.coverage' ||
808
+ nameLower === 'pytest.ini' ||
809
+ nameLower === 'conftest.py') {
810
+ item.setAttribute('data-pytest', 'true');
811
+ }
812
+ // Check if this is a directory (folder)
813
+ const isDir = fileType === 'directory' ||
814
+ item.classList.contains('jp-DirListing-directory');
815
+ if (isDir) {
816
+ // Check if this folder is a Python package (async)
817
+ // Pass just the folder name - checkPythonPackage gets current path from file browser
818
+ checkPythonPackage(name).then(isPythonPackage => {
819
+ if (isPythonPackage) {
820
+ item.setAttribute('data-python-package', 'true');
821
+ }
822
+ else {
823
+ item.removeAttribute('data-python-package');
824
+ }
825
+ });
826
+ }
827
+ else {
828
+ item.removeAttribute('data-python-package');
829
+ }
669
830
  });
670
831
  };
671
832
  // Watch for changes in the file browser
@@ -880,6 +1041,9 @@ const plugin = {
880
1041
  icon: batchIcon
881
1042
  });
882
1043
  }
1044
+ // Note: uv.lock icon is handled via MutationObserver + CSS override
1045
+ // (see injectIconOverrideCSS function) since pattern-only registration
1046
+ // doesn't work reliably for files without standard extensions
883
1047
  };
884
1048
  // Debounce timer for settings change alert
885
1049
  let settingsChangeTimeout = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_vscode_icons_extension",
3
- "version": "1.0.117",
3
+ "version": "1.1.8",
4
4
  "description": "Jupyterlab extension with a shameless rip-off of the vscode-icons into our beloved environment",
5
5
  "keywords": [
6
6
  "jupyter",
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  JupyterFrontEndPlugin
4
4
  } from '@jupyterlab/application';
5
5
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
6
+ import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
6
7
  import { LabIcon } from '@jupyterlab/ui-components';
7
8
  import { getIconSVG } from './icons';
8
9
 
@@ -365,7 +366,8 @@ const fileTypeConfigs: IFileTypeConfig[] = [
365
366
  group: 'enableConfigIcons'
366
367
  },
367
368
  {
368
- pattern: '^(terraform\\.tfvars\\..*|\\.terraform\\.lock\\..*|\\.terraform\\.tfstate\\.lock\\..*)$',
369
+ pattern:
370
+ '^(terraform\\.tfvars\\..*|\\.terraform\\.lock\\..*|\\.terraform\\.tfstate\\.lock\\..*)$',
369
371
  extensions: [],
370
372
  iconName: 'file-type-terraform',
371
373
  group: 'enableConfigIcons'
@@ -395,26 +397,26 @@ const plugin: JupyterFrontEndPlugin<void> = {
395
397
  description:
396
398
  'Jupyterlab extension with a shameless rip-off of the vscode-icons into our beloved environment',
397
399
  autoStart: true,
398
- optional: [ISettingRegistry],
400
+ optional: [ISettingRegistry, IDefaultFileBrowser],
399
401
  activate: (
400
402
  app: JupyterFrontEnd,
401
- settingRegistry: ISettingRegistry | null
403
+ settingRegistry: ISettingRegistry | null,
404
+ defaultFileBrowser: IDefaultFileBrowser | null
402
405
  ) => {
403
406
  const { docRegistry } = app;
404
407
 
405
408
  // Function to inject CSS that overrides Jupytext icons
406
409
  const injectIconOverrideCSS = () => {
407
-
408
410
  // Get icons: Claude (VSCode), Office (VSCode)
409
411
  const claudeIcon = createLabIcon('file-type-claude');
410
412
  const wordIcon = createLabIcon('file-type-word');
411
413
  const excelIcon = createLabIcon('file-type-excel');
412
414
  const powerpointIcon = createLabIcon('file-type-powerpoint');
413
415
 
414
- // Custom Markdown icon (from markdown.svg - purple M with arrow, darker #7a2491)
416
+ // Custom Markdown icon (from markdown.svg - purple M with arrow, #8a2ea5)
415
417
  const markdownSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 309 327">
416
- <path fill="#7a2491" 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"/>
417
- <path fill="#7a2491" 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"/>
418
+ <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"/>
419
+ <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"/>
418
420
  </svg>`;
419
421
 
420
422
  // Custom PDF icon (document with folded corner and red PDF banner)
@@ -451,9 +453,10 @@ const plugin: JupyterFrontEndPlugin<void> = {
451
453
  <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"/>
452
454
  </svg>`;
453
455
 
454
- // Custom README icon (info icon - centered, color #912bac from info2.svg)
456
+ // Custom README icon (info icon - purple background #912bac, gray "i" #bdbdbd)
455
457
  const readmeSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
456
- <path fill="#912bac" d="m 9.5247234,11.42511 v 76.943619 l 2.1166666,2.116667 h 77.809717 l 2.116667,-2.116667 v -76.94362 l -2.116667,-2.1166662 -77.809717,0 z m 36.2115596,9.141612 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"/>
458
+ <rect x="9.5" y="9.3" width="82" height="81.2" rx="4" fill="#912bac"/>
459
+ <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"/>
457
460
  </svg>`;
458
461
 
459
462
  // Get SVG content from VSCode icons
@@ -470,10 +473,27 @@ const plugin: JupyterFrontEndPlugin<void> = {
470
473
  const claudeDataUri = `data:image/svg+xml;base64,${btoa(claudeSvg)}`;
471
474
  const readmeDataUri = `data:image/svg+xml;base64,${btoa(readmeSvg)}`;
472
475
  const pdfDataUri = `data:image/svg+xml;base64,${btoa(pdfSvg)}`;
473
- const wordDataUri = wordSvg ? `data:image/svg+xml;base64,${btoa(wordSvg)}` : '';
474
- const excelDataUri = excelSvg ? `data:image/svg+xml;base64,${btoa(excelSvg)}` : '';
475
- const powerpointDataUri = powerpointSvg ? `data:image/svg+xml;base64,${btoa(powerpointSvg)}` : '';
476
- const svgFileDataUri = svgFileSvg ? `data:image/svg+xml;base64,${btoa(svgFileSvg)}` : '';
476
+ const wordDataUri = wordSvg
477
+ ? `data:image/svg+xml;base64,${btoa(wordSvg)}`
478
+ : '';
479
+ const excelDataUri = excelSvg
480
+ ? `data:image/svg+xml;base64,${btoa(excelSvg)}`
481
+ : '';
482
+ const powerpointDataUri = powerpointSvg
483
+ ? `data:image/svg+xml;base64,${btoa(powerpointSvg)}`
484
+ : '';
485
+ const svgFileDataUri = svgFileSvg
486
+ ? `data:image/svg+xml;base64,${btoa(svgFileSvg)}`
487
+ : '';
488
+ const uvSvg =
489
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 330 330"><rect height="100%" width="100%" rx="66" fill="#26102f"/><path fill="#d256dc" d="M 65,65 h92 v130 h16 v-130 h92 v200 h-16 v-20 h-8 a20,20 0 0 1 -20,20 h-136 a20,20 0 0 1 -20,-20 z"/></svg>';
490
+ const uvDataUri = `data:image/svg+xml;base64,${btoa(uvSvg)}`;
491
+ const pytestSvg =
492
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 474 542"><path fill="#696969" d="M21,43h431c12,0 21,9 21,21c0,12-9,21-21,21H21c-12,0-21-9-21-21c0-12,9-21,21-21z"/><path fill="#009fe3" d="M25,0h87v20H25z"/><path fill="#c7d302" d="M138,0h87v20h-87z"/><path fill="#f07e16" d="M250,0h87v20h-87z"/><path fill="#df2815" d="M362,0h87v20h-87z"/><path fill="#df2815" d="M362,107h87v147h-87z"/><path fill="#f07e16" d="M250,107h87v238h-87z"/><path fill="#c7d302" d="M138,107h87v357h-87z"/><path fill="#009fe3" d="M25,107h87v435h-87z"/></svg>';
493
+ const pytestDataUri = `data:image/svg+xml;base64,${btoa(pytestSvg)}`;
494
+ const pythonPackageSvg =
495
+ '<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
+ const pythonPackageDataUri = `data:image/svg+xml;base64,${btoa(pythonPackageSvg)}`;
477
497
 
478
498
  // Inject CSS that overrides icons for .py and .md files
479
499
  // Note: Jupytext marks .py and .md files as type="notebook", so we need to
@@ -630,6 +650,54 @@ const plugin: JupyterFrontEndPlugin<void> = {
630
650
  background-repeat: no-repeat;
631
651
  background-position: center;
632
652
  }
653
+
654
+ /* Override uv.lock file icon with UV icon */
655
+ .jp-DirListing-item[data-uv-lock] .jp-DirListing-itemIcon svg,
656
+ .jp-DirListing-item[data-uv-lock] .jp-DirListing-itemIcon img {
657
+ display: none !important;
658
+ }
659
+ .jp-DirListing-item[data-uv-lock] .jp-DirListing-itemIcon::before {
660
+ content: '';
661
+ display: inline-block;
662
+ width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
663
+ height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
664
+ background-image: url('${uvDataUri}');
665
+ background-size: contain;
666
+ background-repeat: no-repeat;
667
+ background-position: center;
668
+ }
669
+
670
+ /* Override pytest-related file icons */
671
+ .jp-DirListing-item[data-pytest] .jp-DirListing-itemIcon svg,
672
+ .jp-DirListing-item[data-pytest] .jp-DirListing-itemIcon img {
673
+ display: none !important;
674
+ }
675
+ .jp-DirListing-item[data-pytest] .jp-DirListing-itemIcon::before {
676
+ content: '';
677
+ display: inline-block;
678
+ width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
679
+ height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
680
+ background-image: url('${pytestDataUri}');
681
+ background-size: contain;
682
+ background-repeat: no-repeat;
683
+ background-position: center;
684
+ }
685
+
686
+ /* Override Python package folder icons */
687
+ .jp-DirListing-item[data-python-package] .jp-DirListing-itemIcon svg,
688
+ .jp-DirListing-item[data-python-package] .jp-DirListing-itemIcon img {
689
+ display: none !important;
690
+ }
691
+ .jp-DirListing-item[data-python-package] .jp-DirListing-itemIcon::before {
692
+ content: '';
693
+ display: inline-block;
694
+ width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
695
+ height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
696
+ background-image: url('${pythonPackageDataUri}');
697
+ background-size: contain;
698
+ background-repeat: no-repeat;
699
+ background-position: center;
700
+ }
633
701
  `;
634
702
 
635
703
  // Add CSS to make JavaScript and .env icons less bright
@@ -653,6 +721,76 @@ const plugin: JupyterFrontEndPlugin<void> = {
653
721
  }
654
722
  `;
655
723
 
724
+ // Cache for Python package folder checks
725
+ const pythonPackageCache: Map<string, boolean> = new Map();
726
+
727
+ // Folders to exclude from Python package checking
728
+ const excludedFolderPatterns = [
729
+ /^\.git$/,
730
+ /^\.hg$/,
731
+ /^\.svn$/,
732
+ /^\.venv$/,
733
+ /^venv$/,
734
+ /^\.env$/,
735
+ /^env$/,
736
+ /^node_modules$/,
737
+ /^__pycache__$/,
738
+ /^\.ipynb_checkpoints$/,
739
+ /^\.pytest_cache$/,
740
+ /^\.mypy_cache$/,
741
+ /^\.ruff_cache$/,
742
+ /^\.tox$/,
743
+ /^\.nox$/,
744
+ /^\.coverage$/,
745
+ /^htmlcov$/,
746
+ /^dist$/,
747
+ /^build$/,
748
+ /^\.eggs$/,
749
+ /\.egg-info$/,
750
+ /\.dist-info$/
751
+ ];
752
+
753
+ const isExcludedFolder = (name: string): boolean => {
754
+ return excludedFolderPatterns.some(pattern => pattern.test(name));
755
+ };
756
+
757
+ // Check if a folder is a Python package (contains __init__.py)
758
+ // Use the file browser's model to get the correct path
759
+ const checkPythonPackage = async (
760
+ folderName: string
761
+ ): Promise<boolean> => {
762
+ // Skip excluded folders
763
+ if (isExcludedFolder(folderName)) {
764
+ return false;
765
+ }
766
+
767
+ // Get the current path from the file browser model
768
+ if (!defaultFileBrowser) {
769
+ return false;
770
+ }
771
+ const currentPath = defaultFileBrowser.model.path;
772
+ const fullPath = currentPath
773
+ ? `${currentPath}/${folderName}`
774
+ : folderName;
775
+
776
+ if (pythonPackageCache.has(fullPath)) {
777
+ return pythonPackageCache.get(fullPath)!;
778
+ }
779
+ try {
780
+ // Use JupyterLab's contents manager with the correct path from file browser
781
+ const contents = app.serviceManager.contents;
782
+ const model = await contents.get(fullPath, { content: true });
783
+ const hasInit =
784
+ model.content?.some((item: any) => item.name === '__init__.py') ||
785
+ false;
786
+ pythonPackageCache.set(fullPath, hasInit);
787
+ return hasInit;
788
+ } catch {
789
+ pythonPackageCache.set(fullPath, false);
790
+ return false;
791
+ }
792
+ };
793
+
656
794
  // Add a MutationObserver to mark special files in the file browser
657
795
  const markSpecialFiles = () => {
658
796
  // Process ALL items - clear wrong attributes and set correct ones
@@ -707,11 +845,21 @@ const plugin: JupyterFrontEndPlugin<void> = {
707
845
  // Set the correct attribute based on extension
708
846
  if (nameLower.endsWith('.pdf')) {
709
847
  item.setAttribute('data-vscode-pdf', 'true');
710
- } else if (nameLower.endsWith('.doc') || nameLower.endsWith('.docx')) {
848
+ } else if (
849
+ nameLower.endsWith('.doc') ||
850
+ nameLower.endsWith('.docx')
851
+ ) {
711
852
  item.setAttribute('data-vscode-word', 'true');
712
- } else if (nameLower.endsWith('.xls') || nameLower.endsWith('.xlsx') || nameLower.endsWith('.xlsm')) {
853
+ } else if (
854
+ nameLower.endsWith('.xls') ||
855
+ nameLower.endsWith('.xlsx') ||
856
+ nameLower.endsWith('.xlsm')
857
+ ) {
713
858
  item.setAttribute('data-vscode-excel', 'true');
714
- } else if (nameLower.endsWith('.ppt') || nameLower.endsWith('.pptx')) {
859
+ } else if (
860
+ nameLower.endsWith('.ppt') ||
861
+ nameLower.endsWith('.pptx')
862
+ ) {
715
863
  item.setAttribute('data-vscode-powerpoint', 'true');
716
864
  }
717
865
 
@@ -720,6 +868,40 @@ const plugin: JupyterFrontEndPlugin<void> = {
720
868
  if (nameLower.endsWith('.svg')) {
721
869
  item.setAttribute('data-vscode-svg-override', 'true');
722
870
  }
871
+
872
+ // Force UV icon for uv.lock file
873
+ item.removeAttribute('data-uv-lock');
874
+ if (nameLower === 'uv.lock') {
875
+ item.setAttribute('data-uv-lock', 'true');
876
+ }
877
+
878
+ // Force pytest icon for pytest-related files
879
+ item.removeAttribute('data-pytest');
880
+ if (
881
+ nameLower === '.coverage' ||
882
+ nameLower === 'pytest.ini' ||
883
+ nameLower === 'conftest.py'
884
+ ) {
885
+ item.setAttribute('data-pytest', 'true');
886
+ }
887
+
888
+ // Check if this is a directory (folder)
889
+ const isDir =
890
+ fileType === 'directory' ||
891
+ item.classList.contains('jp-DirListing-directory');
892
+ if (isDir) {
893
+ // Check if this folder is a Python package (async)
894
+ // Pass just the folder name - checkPythonPackage gets current path from file browser
895
+ checkPythonPackage(name).then(isPythonPackage => {
896
+ if (isPythonPackage) {
897
+ item.setAttribute('data-python-package', 'true');
898
+ } else {
899
+ item.removeAttribute('data-python-package');
900
+ }
901
+ });
902
+ } else {
903
+ item.removeAttribute('data-python-package');
904
+ }
723
905
  });
724
906
  };
725
907
 
@@ -971,6 +1153,10 @@ const plugin: JupyterFrontEndPlugin<void> = {
971
1153
  icon: batchIcon
972
1154
  });
973
1155
  }
1156
+
1157
+ // Note: uv.lock icon is handled via MutationObserver + CSS override
1158
+ // (see injectIconOverrideCSS function) since pattern-only registration
1159
+ // doesn't work reliably for files without standard extensions
974
1160
  };
975
1161
 
976
1162
  // Debounce timer for settings change alert
@@ -1000,7 +1186,6 @@ const plugin: JupyterFrontEndPlugin<void> = {
1000
1186
  }
1001
1187
  });
1002
1188
 
1003
-
1004
1189
  // Debounce the alert to show only once when multiple settings change
1005
1190
  if (settingsChangeTimeout) {
1006
1191
  clearTimeout(settingsChangeTimeout);
package/style/base.css CHANGED
@@ -47,5 +47,5 @@
47
47
 
48
48
  /* Make hidden items darker (items starting with .) */
49
49
  .jp-DirListing-item[data-is-dot] {
50
- opacity: 55% !important;
50
+ opacity: 0.55 !important;
51
51
  }