@@ -88,6 +88,18 @@ function getEncodedRootLength(path: string): number {
8888 return p1 + 1 ; // UNC: "//server/" or "\\server\"
8989 }
9090
91+ // Dynamic/virtual compiler file name.
92+ if ( ch0 === 94 /* ^ */ && path . charCodeAt ( 1 ) === CharacterCodesSlash ) {
93+ if ( path . startsWith ( dynamicURIFileNamePrefix ) ) {
94+ const schemeEnd = path . indexOf ( directorySeparator , dynamicURIFileNamePrefix . length ) ;
95+ if ( schemeEnd !== - 1 ) {
96+ const authorityEnd = path . indexOf ( directorySeparator , schemeEnd + 1 ) ;
97+ return authorityEnd === - 1 ? path . length : authorityEnd + 1 ;
98+ }
99+ }
100+ return 2 ;
101+ }
102+
91103 // DOS
92104 if ( isVolumeCharacter ( ch0 ) && path . charCodeAt ( 1 ) === CharacterCodesColon ) {
93105 const ch2 = path . charCodeAt ( 2 ) ;
@@ -109,7 +121,7 @@ function getEncodedRootLength(path: string): number {
109121 const scheme = path . slice ( 0 , schemeEnd ) ;
110122 const authority = path . slice ( authorityStart , authorityEnd ) ;
111123 if (
112- scheme === "file" && ( authority === "" || authority === "localhost" ) &&
124+ scheme . toLowerCase ( ) === "file" && ( authority === "" || authority . toLowerCase ( ) === "localhost" ) &&
113125 isVolumeCharacter ( path . charCodeAt ( authorityEnd + 1 ) )
114126 ) {
115127 const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd ( path , authorityEnd + 2 ) ;
@@ -391,7 +403,9 @@ export function toPath(fileName: string, basePath: string | undefined, getCanoni
391403 const nonCanonicalizedPath = isRootedDiskPath ( fileName )
392404 ? normalizePath ( fileName )
393405 : getNormalizedAbsolutePath ( fileName , basePath ) ;
394- return getCanonicalFileName ( nonCanonicalizedPath ) ;
406+ return nonCanonicalizedPath . startsWith ( dynamicURIFileNamePrefix )
407+ ? nonCanonicalizedPath
408+ : getCanonicalFileName ( nonCanonicalizedPath ) ;
395409}
396410
397411/**
@@ -410,6 +424,11 @@ function toLowerCase(s: string): string {
410424}
411425
412426const bundledScheme = "bundled:///" ;
427+ const dynamicURIFileNamePrefix = "^/~ts-uri~/" ;
428+ const dynamicURIPathSegmentEscapePrefix = "~ts-uri-escape~" ;
429+ const dynamicURIModuleSpecifierEscapePrefix = "~ts-uri-spec~" ;
430+ const dynamicURINoPathEscapePrefix = "~ts-uri-no-path~" ;
431+ const dynamicURIPathSegmentEscapeRegExp = / (?: ^ | \/ ) (?: \. { 1 , 2 } (?: \/ | $ ) | ~ t s - u r i - e s c a p e ~ | ~ t s - u r i - s p e c ~ | ~ t s - u r i - n o - p a t h ~ ) / ;
413432
414433/**
415434 * Returns true if the path refers to a bundled library file.
@@ -493,8 +512,8 @@ export function fileNameToDocumentURI(fileName: string): string {
493512
494513 // Dynamic/virtual files (untitled, vscode-vfs, etc.) need special handling
495514 if ( isDynamicFileName ( fileName ) ) {
496- // Format: ^/scheme/authority/path
497- const withoutPrefix = fileName . substring ( 2 ) ; // Remove "^/"
515+ const encoded = fileName . startsWith ( dynamicURIFileNamePrefix ) ;
516+ const withoutPrefix = fileName . substring ( encoded ? dynamicURIFileNamePrefix . length : 2 ) ;
498517 const firstSlash = withoutPrefix . indexOf ( "/" ) ;
499518 if ( firstSlash === - 1 ) {
500519 throw new Error ( "invalid file name: " + fileName ) ;
@@ -506,11 +525,20 @@ export function fileNameToDocumentURI(fileName: string): string {
506525 if ( secondSlash === - 1 ) {
507526 throw new Error ( "invalid file name: " + fileName ) ;
508527 }
509- const authority = rest . substring ( 0 , secondSlash ) ;
510- const path = rest . substring ( secondSlash + 1 ) ;
528+ const encodedAuthority = rest . substring ( 0 , secondSlash ) ;
529+ const hasAuthority = encodedAuthority !== "ts-nul-authority" ;
530+ const authority = encoded ? decodeDynamicURIPathSegment ( encodedAuthority ) : encodedAuthority ;
531+ const encodedPath = rest . substring ( secondSlash + 1 ) ;
532+ if ( encoded && hasAuthority ) {
533+ const suffix = decodeDynamicURINoPath ( encodedPath ) ;
534+ if ( suffix !== undefined ) {
535+ return scheme + "://" + authority + suffix ;
536+ }
537+ }
538+ const path = encoded ? decodeDynamicURIPath ( encodedPath ) : encodedPath ;
511539
512540 // ts-nul-authority is a placeholder for URIs without an authority
513- if ( authority === "ts-nul-authority" ) {
541+ if ( ! hasAuthority ) {
514542 return scheme + ":" + path ;
515543 }
516544 return scheme + "://" + authority + "/" + path ;
@@ -585,17 +613,188 @@ export function documentURIToFileName(uri: string): string {
585613
586614 const scheme = uri . substring ( 0 , colonIndex ) ;
587615 let path = uri . substring ( colonIndex + 1 ) ;
616+ let suffix = "" ;
617+ const suffixStart = path . search ( / [ ? # ] / ) ;
618+ if ( suffixStart !== - 1 ) {
619+ suffix = path . substring ( suffixStart ) ;
620+ path = path . substring ( 0 , suffixStart ) ;
621+ }
588622
589623 let authority = "ts-nul-authority" ;
624+ let hasAuthority = false ;
625+ let hasPath = true ;
590626 if ( path . startsWith ( "//" ) ) {
627+ hasAuthority = true ;
591628 const rest = path . substring ( 2 ) ;
592629 const slashIndex = rest . indexOf ( "/" ) ;
593630 if ( slashIndex === - 1 ) {
594- throw new Error ( "invalid URI: " + uri ) ;
631+ authority = rest ;
632+ path = "" ;
633+ hasPath = false ;
634+ }
635+ else {
636+ authority = rest . substring ( 0 , slashIndex ) ;
637+ path = rest . substring ( slashIndex + 1 ) ;
638+ }
639+ }
640+
641+ let encodedAuthority = authority ;
642+ if ( hasAuthority ) {
643+ encodedAuthority = authority === "ts-nul-authority"
644+ ? forceEncodeDynamicURIPathSegment ( authority , false )
645+ : encodeDynamicURIPath ( authority ) ;
646+ }
647+ const encodedPath = hasPath
648+ ? encodeDynamicURIPathWithSuffix ( path , suffix )
649+ : encodeDynamicURINoPath ( suffix ) ;
650+ return dynamicURIFileNamePrefix + scheme + "/" + encodedAuthority + "/" + encodedPath ;
651+ }
652+
653+ function encodeDynamicURIPath ( path : string , preserveFinalExtension = true ) : string {
654+ if ( ! dynamicURIPathNeedsEncoding ( path ) && ! isRootedDiskPath ( path ) ) {
655+ return path ;
656+ }
657+ const segments = path . split ( "/" ) ;
658+ for ( let i = 0 ; i < segments . length ; i ++ ) {
659+ segments [ i ] = encodeDynamicURIPathSegment ( segments [ i ] , preserveFinalExtension && i === segments . length - 1 ) ;
660+ }
661+ if ( isRootedDiskPath ( segments . join ( "/" ) ) ) {
662+ segments [ 0 ] = forceEncodeDynamicURIPathSegment ( segments [ 0 ] , segments . length === 1 && preserveFinalExtension ) ;
663+ }
664+ return segments . join ( "/" ) ;
665+ }
666+
667+ function encodeDynamicURIPathWithSuffix ( path : string , suffix : string ) : string {
668+ if ( suffix === "" ) {
669+ return encodeDynamicURIPath ( path ) ;
670+ }
671+ const slash = path . lastIndexOf ( "/" ) ;
672+ const before = slash === - 1 ? "" : encodeDynamicURIPath ( path . substring ( 0 , slash ) , false ) + "/" ;
673+ return before + forceEncodeDynamicURIPathSegmentWithSuffix ( path . substring ( slash + 1 ) , suffix ) ;
674+ }
675+
676+ function dynamicURIPathNeedsEncoding ( path : string ) : boolean {
677+ return path === "" ||
678+ path . startsWith ( "/" ) ||
679+ path . endsWith ( "/" ) ||
680+ path . includes ( "//" ) ||
681+ path . includes ( "\\" ) ||
682+ dynamicURIPathSegmentEscapeRegExp . test ( path ) ;
683+ }
684+
685+ function dynamicURIPathSegmentNeedsEncoding ( segment : string ) : boolean {
686+ return segment === "" ||
687+ segment === "." ||
688+ segment === ".." ||
689+ segment . startsWith ( dynamicURIPathSegmentEscapePrefix ) ||
690+ segment . startsWith ( dynamicURIModuleSpecifierEscapePrefix ) ||
691+ segment . startsWith ( dynamicURINoPathEscapePrefix ) ||
692+ segment . includes ( "\\" ) ;
693+ }
694+
695+ function encodeDynamicURIPathSegment ( segment : string , preserveExtension : boolean ) : string {
696+ return dynamicURIPathSegmentNeedsEncoding ( segment )
697+ ? forceEncodeDynamicURIPathSegment ( segment , preserveExtension )
698+ : segment ;
699+ }
700+
701+ function forceEncodeDynamicURIPathSegment ( segment : string , preserveExtension : boolean ) : string {
702+ let extension = "" ;
703+ if ( preserveExtension && segment !== "." && segment !== ".." ) {
704+ [ segment , extension ] = splitDynamicURIFileExtension ( segment ) ;
705+ }
706+ return dynamicURIPathSegmentEscapePrefix + encodeDynamicURIHex ( segment ) + "~" + extension ;
707+ }
708+
709+ function forceEncodeDynamicURIPathSegmentWithSuffix ( segment : string , suffix : string ) : string {
710+ const [ base , extension ] = splitDynamicURIFileExtension ( segment ) ;
711+ return forceEncodeDynamicURIPathSegment ( base + "\0" + suffix , false ) + extension ;
712+ }
713+
714+ function splitDynamicURIFileExtension ( segment : string ) : [ base : string , extension : string ] {
715+ const extension = getDynamicURIFileExtension ( segment ) ;
716+ return extension === ""
717+ ? [ segment , extension ]
718+ : [ segment . substring ( 0 , segment . length - extension . length ) , extension ] ;
719+ }
720+
721+ function getDynamicURIFileExtension ( segment : string ) : string {
722+ const baseStart = segment . lastIndexOf ( "\\" ) + 1 ;
723+ for ( const extension of [ ".d.ts" , ".d.mts" , ".d.cts" ] ) {
724+ if ( segment . endsWith ( extension ) && segment . length - extension . length >= baseStart ) {
725+ return extension ;
726+ }
727+ }
728+ if ( segment . endsWith ( ".ts" ) ) {
729+ const declaration = segment . indexOf ( ".d." , baseStart ) ;
730+ if ( declaration !== - 1 ) {
731+ return segment . substring ( declaration ) ;
595732 }
596- authority = rest . substring ( 0 , slashIndex ) ;
597- path = rest . substring ( slashIndex + 1 ) ;
598733 }
734+ const dot = segment . lastIndexOf ( "." ) ;
735+ return dot >= baseStart ? segment . substring ( dot ) : "" ;
736+ }
737+
738+ function encodeDynamicURIHex ( text : string ) : string {
739+ let hex = "" ;
740+ const encoded = encodeURIComponent ( text ) ;
741+ for ( let i = 0 ; i < encoded . length ; i ++ ) {
742+ if ( encoded . charCodeAt ( i ) === CharacterCodesPercent ) {
743+ hex += encoded . slice ( i + 1 , i + 3 ) . toLowerCase ( ) ;
744+ i += 2 ;
745+ }
746+ else {
747+ hex += encoded . charCodeAt ( i ) . toString ( 16 ) . padStart ( 2 , "0" ) ;
748+ }
749+ }
750+ return hex ;
751+ }
599752
600- return "^/" + scheme + "/" + authority + "/" + path ;
753+ function decodeDynamicURIPath ( path : string ) : string {
754+ return path . includes ( dynamicURIPathSegmentEscapePrefix )
755+ ? path . split ( "/" ) . map ( decodeDynamicURIPathSegment ) . join ( "/" )
756+ : path ;
757+ }
758+
759+ function decodeDynamicURIPathSegment ( segment : string ) : string {
760+ if ( ! segment . startsWith ( dynamicURIPathSegmentEscapePrefix ) ) {
761+ return segment ;
762+ }
763+ const separator = segment . indexOf ( "~" , dynamicURIPathSegmentEscapePrefix . length ) ;
764+ if ( separator === - 1 ) {
765+ return segment ;
766+ }
767+ const encoded = segment . slice ( dynamicURIPathSegmentEscapePrefix . length , separator ) ;
768+ const extension = segment . slice ( separator + 1 ) ;
769+ const decoded = decodeDynamicURIHex ( encoded ) ;
770+ if ( decoded === undefined ) {
771+ return segment ;
772+ }
773+ const suffix = decoded . indexOf ( "\0" ) ;
774+ return suffix === - 1
775+ ? decoded + extension
776+ : decoded . substring ( 0 , suffix ) + extension + decoded . substring ( suffix + 1 ) ;
777+ }
778+
779+ function encodeDynamicURINoPath ( suffix : string ) : string {
780+ return dynamicURINoPathEscapePrefix + encodeDynamicURIHex ( suffix ) + "~" ;
781+ }
782+
783+ function decodeDynamicURINoPath ( path : string ) : string | undefined {
784+ if ( ! path . startsWith ( dynamicURINoPathEscapePrefix ) || ! path . endsWith ( "~" ) ) {
785+ return undefined ;
786+ }
787+ return decodeDynamicURIHex ( path . substring ( dynamicURINoPathEscapePrefix . length , path . length - 1 ) ) ;
788+ }
789+
790+ function decodeDynamicURIHex ( encoded : string ) : string | undefined {
791+ if ( encoded . length % 2 !== 0 || ! / ^ [ 0 - 9 a - f ] * $ / i. test ( encoded ) ) {
792+ return undefined ;
793+ }
794+ try {
795+ return decodeURIComponent ( encoded . replace ( / ../ g, value => `%${ value } ` ) ) ;
796+ }
797+ catch {
798+ return undefined ;
799+ }
601800}
0 commit comments