@@ -1932,6 +1932,207 @@ namespace ts {
1932
1932
return text1 ? Comparison . GreaterThan : Comparison . LessThan ;
1933
1933
}
1934
1934
1935
+ /**
1936
+ * Given a name and a list of names that are *not* equal to the name, return a spelling suggestion if there is one that is close enough.
1937
+ * Names less than length 3 only check for case-insensitive equality, not Levenshtein distance.
1938
+ *
1939
+ * If there is a candidate that's the same except for case, return that.
1940
+ * If there is a candidate that's within one edit of the name, return that.
1941
+ * Otherwise, return the candidate with the smallest Levenshtein distance,
1942
+ * except for candidates:
1943
+ * * With no name
1944
+ * * Whose length differs from the target name by more than 0.34 of the length of the name.
1945
+ * * Whose levenshtein distance is more than 0.4 of the length of the name
1946
+ * (0.4 allows 1 substitution/transposition for every 5 characters,
1947
+ * and 1 insertion/deletion at 3 characters)
1948
+ */
1949
+ export function getSpellingSuggestion < T > ( name : string , candidates : T [ ] , getName : ( candidate : T ) => string | undefined ) : T | undefined {
1950
+ const maximumLengthDifference = Math . min ( 2 , Math . floor ( name . length * 0.34 ) ) ;
1951
+ let bestDistance = Math . floor ( name . length * 0.4 ) + 1 ; // If the best result isn't better than this, don't bother.
1952
+ let bestCandidate : T | undefined ;
1953
+ let justCheckExactMatches = false ;
1954
+ const nameLowerCase = name . toLowerCase ( ) ;
1955
+ for ( const candidate of candidates ) {
1956
+ const candidateName = getName ( candidate ) ;
1957
+ if ( candidateName !== undefined && Math . abs ( candidateName . length - nameLowerCase . length ) <= maximumLengthDifference ) {
1958
+ const candidateNameLowerCase = candidateName . toLowerCase ( ) ;
1959
+ if ( candidateNameLowerCase === nameLowerCase ) {
1960
+ return candidate ;
1961
+ }
1962
+ if ( justCheckExactMatches ) {
1963
+ continue ;
1964
+ }
1965
+ if ( candidateName . length < 3 ) {
1966
+ // Don't bother, user would have noticed a 2-character name having an extra character
1967
+ continue ;
1968
+ }
1969
+ // Only care about a result better than the best so far.
1970
+ const distance = levenshteinWithMax ( nameLowerCase , candidateNameLowerCase , bestDistance - 1 ) ;
1971
+ if ( distance === undefined ) {
1972
+ continue ;
1973
+ }
1974
+ if ( distance < 3 ) {
1975
+ justCheckExactMatches = true ;
1976
+ bestCandidate = candidate ;
1977
+ }
1978
+ else {
1979
+ Debug . assert ( distance < bestDistance ) ; // Else `levenshteinWithMax` should return undefined
1980
+ bestDistance = distance ;
1981
+ bestCandidate = candidate ;
1982
+ }
1983
+ }
1984
+ }
1985
+ return bestCandidate ;
1986
+ }
1987
+
1988
+ function levenshteinWithMax ( s1 : string , s2 : string , max : number ) : number | undefined {
1989
+ let previous = new Array ( s2 . length + 1 ) ;
1990
+ let current = new Array ( s2 . length + 1 ) ;
1991
+ /** Represents any value > max. We don't care about the particular value. */
1992
+ const big = max + 1 ;
1993
+
1994
+ for ( let i = 0 ; i <= s2 . length ; i ++ ) {
1995
+ previous [ i ] = i ;
1996
+ }
1997
+
1998
+ for ( let i = 1 ; i <= s1 . length ; i ++ ) {
1999
+ const c1 = s1 . charCodeAt ( i - 1 ) ;
2000
+ const minJ = i > max ? i - max : 1 ;
2001
+ const maxJ = s2 . length > max + i ? max + i : s2 . length ;
2002
+ current [ 0 ] = i ;
2003
+ /** Smallest value of the matrix in the ith column. */
2004
+ let colMin = i ;
2005
+ for ( let j = 1 ; j < minJ ; j ++ ) {
2006
+ current [ j ] = big ;
2007
+ }
2008
+ for ( let j = minJ ; j <= maxJ ; j ++ ) {
2009
+ const dist = c1 === s2 . charCodeAt ( j - 1 )
2010
+ ? previous [ j - 1 ]
2011
+ : Math . min ( /*delete*/ previous [ j ] + 1 , /*insert*/ current [ j - 1 ] + 1 , /*substitute*/ previous [ j - 1 ] + 2 ) ;
2012
+ current [ j ] = dist ;
2013
+ colMin = Math . min ( colMin , dist ) ;
2014
+ }
2015
+ for ( let j = maxJ + 1 ; j <= s2 . length ; j ++ ) {
2016
+ current [ j ] = big ;
2017
+ }
2018
+ if ( colMin > max ) {
2019
+ // Give up -- everything in this column is > max and it can't get better in future columns.
2020
+ return undefined ;
2021
+ }
2022
+
2023
+ const temp = previous ;
2024
+ previous = current ;
2025
+ current = temp ;
2026
+ }
2027
+
2028
+ const res = previous [ s2 . length ] ;
2029
+ return res > max ? undefined : res ;
2030
+ }
2031
+
2032
+ export function normalizeSlashes ( path : string ) : string {
2033
+ return path . replace ( / \\ / g, "/" ) ;
2034
+ }
2035
+
2036
+ /**
2037
+ * Returns length of path root (i.e. length of "/", "x:/", "//server/share/, file:///user/files")
2038
+ */
2039
+ export function getRootLength ( path : string ) : number {
2040
+ if ( path . charCodeAt ( 0 ) === CharacterCodes . slash ) {
2041
+ if ( path . charCodeAt ( 1 ) !== CharacterCodes . slash ) return 1 ;
2042
+ const p1 = path . indexOf ( "/" , 2 ) ;
2043
+ if ( p1 < 0 ) return 2 ;
2044
+ const p2 = path . indexOf ( "/" , p1 + 1 ) ;
2045
+ if ( p2 < 0 ) return p1 + 1 ;
2046
+ return p2 + 1 ;
2047
+ }
2048
+ if ( path . charCodeAt ( 1 ) === CharacterCodes . colon ) {
2049
+ if ( path . charCodeAt ( 2 ) === CharacterCodes . slash || path . charCodeAt ( 2 ) === CharacterCodes . backslash ) return 3 ;
2050
+ }
2051
+ // Per RFC 1738 'file' URI schema has the shape file://<host>/<path>
2052
+ // if <host> is omitted then it is assumed that host value is 'localhost',
2053
+ // however slash after the omitted <host> is not removed.
2054
+ // file:///folder1/file1 - this is a correct URI
2055
+ // file://folder2/file2 - this is an incorrect URI
2056
+ if ( path . lastIndexOf ( "file:///" , 0 ) === 0 ) {
2057
+ return "file:///" . length ;
2058
+ }
2059
+ const idx = path . indexOf ( "://" ) ;
2060
+ if ( idx !== - 1 ) {
2061
+ return idx + "://" . length ;
2062
+ }
2063
+ return 0 ;
2064
+ }
2065
+
2066
+ /**
2067
+ * Internally, we represent paths as strings with '/' as the directory separator.
2068
+ * When we make system calls (eg: LanguageServiceHost.getDirectory()),
2069
+ * we expect the host to correctly handle paths in our specified format.
2070
+ */
2071
+ export const directorySeparator = "/" ;
2072
+ const directorySeparatorCharCode = CharacterCodes . slash ;
2073
+ function getNormalizedParts ( normalizedSlashedPath : string , rootLength : number ) : string [ ] {
2074
+ const parts = normalizedSlashedPath . substr ( rootLength ) . split ( directorySeparator ) ;
2075
+ const normalized : string [ ] = [ ] ;
2076
+ for ( const part of parts ) {
2077
+ if ( part !== "." ) {
2078
+ if ( part === ".." && normalized . length > 0 && lastOrUndefined ( normalized ) !== ".." ) {
2079
+ normalized . pop ( ) ;
2080
+ }
2081
+ else {
2082
+ // A part may be an empty string (which is 'falsy') if the path had consecutive slashes,
2083
+ // e.g. "path//file.ts". Drop these before re-joining the parts.
2084
+ if ( part ) {
2085
+ normalized . push ( part ) ;
2086
+ }
2087
+ }
2088
+ }
2089
+ }
2090
+
2091
+ return normalized ;
2092
+ }
2093
+
2094
+ export function normalizePath ( path : string ) : string {
2095
+ return normalizePathAndParts ( path ) . path ;
2096
+ }
2097
+
2098
+ export function normalizePathAndParts ( path : string ) : { path : string , parts : string [ ] } {
2099
+ path = normalizeSlashes ( path ) ;
2100
+ const rootLength = getRootLength ( path ) ;
2101
+ const root = path . substr ( 0 , rootLength ) ;
2102
+ const parts = getNormalizedParts ( path , rootLength ) ;
2103
+ if ( parts . length ) {
2104
+ const joinedParts = root + parts . join ( directorySeparator ) ;
2105
+ return { path : pathEndsWithDirectorySeparator ( path ) ? joinedParts + directorySeparator : joinedParts , parts } ;
2106
+ }
2107
+ else {
2108
+ return { path : root , parts } ;
2109
+ }
2110
+ }
2111
+
2112
+ /** A path ending with '/' refers to a directory only, never a file. */
2113
+ export function pathEndsWithDirectorySeparator ( path : string ) : boolean {
2114
+ return path . charCodeAt ( path . length - 1 ) === directorySeparatorCharCode ;
2115
+ }
2116
+
2117
+ /**
2118
+ * Returns the path except for its basename. Eg:
2119
+ *
2120
+ * /path/to/file.ext -> /path/to
2121
+ */
2122
+ export function getDirectoryPath ( path : Path ) : Path ;
2123
+ export function getDirectoryPath ( path : string ) : string ;
2124
+ export function getDirectoryPath ( path : string ) : string {
2125
+ return path . substr ( 0 , Math . max ( getRootLength ( path ) , path . lastIndexOf ( directorySeparator ) ) ) ;
2126
+ }
2127
+
2128
+ export function isUrl ( path : string ) {
2129
+ return path && ! isRootedDiskPath ( path ) && stringContains ( path , "://" ) ;
2130
+ }
2131
+
2132
+ export function pathIsRelative ( path : string ) : boolean {
2133
+ return / ^ \. \. ? ( $ | [ \\ / ] ) / . test ( path ) ;
2134
+ }
2135
+
1935
2136
export function getEmitScriptTarget ( compilerOptions : CompilerOptions ) {
1936
2137
return compilerOptions . target || ScriptTarget . ES3 ;
1937
2138
}
0 commit comments