Skip to content

Commit 453ebc3

Browse files
authored
feat: support uri path mappings (#879)
* Re-implement path mapping logic * Fix typos. * Test Phar specific cases * Add exact file mapping match. * Changelog.
1 parent af38cb8 commit 453ebc3

File tree

4 files changed

+253
-126
lines changed

4 files changed

+253
-126
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## [1.31.0]
8+
9+
- Allow more flexible path mappings in url format.
10+
711
## [1.30.0]
812

913
- Add skipFiles launch setting to skip over specified file patterns.

src/paths.ts

Lines changed: 108 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,149 +1,153 @@
11
import fileUrl from 'file-url'
22
import * as url from 'url'
33
import * as path from 'path'
4-
import { decode } from 'urlencode'
5-
import RelateUrl from 'relateurl'
64
import minimatch from 'minimatch'
75

8-
/**
9-
* Options to make sure that RelateUrl only outputs relative URLs and performs not other "smart" modifications.
10-
* They would mess up things like prefix checking.
11-
*/
12-
const RELATE_URL_OPTIONS: RelateUrl.Options = {
13-
// Make sure RelateUrl does not prefer root-relative URLs if shorter
14-
output: RelateUrl.PATH_RELATIVE,
15-
// Make sure RelateUrl does not remove trailing slash if present
16-
removeRootTrailingSlash: false,
17-
// Make sure RelateUrl does not remove default ports
18-
defaultPorts: {},
19-
}
20-
21-
/**
22-
* Like `path.relative()` but for URLs.
23-
* Inverse of `url.resolve()` or `new URL(relative, base)`.
24-
*/
25-
const relativeUrl = (from: string, to: string): string => RelateUrl.relate(from, to, RELATE_URL_OPTIONS)
26-
276
/** converts a server-side Xdebug file URI to a local path for VS Code with respect to source root settings */
28-
export function convertDebuggerPathToClient(
29-
fileUri: string | url.Url,
30-
pathMapping?: { [index: string]: string }
31-
): string {
32-
let localSourceRoot: string | undefined
33-
let serverSourceRoot: string | undefined
34-
if (typeof fileUri === 'string') {
35-
fileUri = url.parse(fileUri)
36-
}
37-
// convert the file URI to a path
38-
let serverPath = decode(fileUri.pathname!)
39-
// strip the trailing slash from Windows paths (indicated by a drive letter with a colon)
40-
const serverIsWindows = /^\/[a-zA-Z]:\//.test(serverPath)
41-
if (serverIsWindows) {
42-
serverPath = serverPath.substr(1)
43-
}
7+
export function convertDebuggerPathToClient(fileUri: string, pathMapping?: { [index: string]: string }): string {
8+
let localSourceRootUrl: string | undefined
9+
let serverSourceRootUrl: string | undefined
10+
4411
if (pathMapping) {
4512
for (const mappedServerPath of Object.keys(pathMapping)) {
46-
const mappedLocalSource = pathMapping[mappedServerPath]
47-
// normalize slashes for windows-to-unix
48-
const serverRelative = (serverIsWindows ? path.win32 : path.posix).relative(mappedServerPath, serverPath)
49-
if (!serverRelative.startsWith('..')) {
13+
let mappedServerPathUrl = pathOrUrlToUrl(mappedServerPath)
14+
// try exact match
15+
if (fileUri.length === mappedServerPathUrl.length && isSameUri(fileUri, mappedServerPathUrl)) {
16+
// bail early
17+
serverSourceRootUrl = mappedServerPathUrl
18+
localSourceRootUrl = pathOrUrlToUrl(pathMapping[mappedServerPath])
19+
break
20+
}
21+
// make sure it ends with a slash
22+
if (!mappedServerPathUrl.endsWith('/')) {
23+
mappedServerPathUrl += '/'
24+
}
25+
if (isSameUri(fileUri.substring(0, mappedServerPathUrl.length), mappedServerPathUrl)) {
5026
// If a matching mapping has previously been found, only update
5127
// it if the current server path is longer than the previous one
5228
// (longest prefix matching)
53-
if (!serverSourceRoot || mappedServerPath.length > serverSourceRoot.length) {
54-
serverSourceRoot = mappedServerPath
55-
localSourceRoot = mappedLocalSource
29+
if (!serverSourceRootUrl || mappedServerPathUrl.length > serverSourceRootUrl.length) {
30+
serverSourceRootUrl = mappedServerPathUrl
31+
localSourceRootUrl = pathOrUrlToUrl(pathMapping[mappedServerPath])
32+
if (!localSourceRootUrl.endsWith('/')) {
33+
localSourceRootUrl += '/'
34+
}
5635
}
5736
}
5837
}
5938
}
6039
let localPath: string
61-
if (serverSourceRoot && localSourceRoot) {
62-
const clientIsWindows =
63-
/^[a-zA-Z]:\\/.test(localSourceRoot) ||
64-
/^\\\\/.test(localSourceRoot) ||
65-
/^[a-zA-Z]:$/.test(localSourceRoot) ||
66-
/^[a-zA-Z]:\//.test(localSourceRoot)
67-
// get the part of the path that is relative to the source root
68-
let pathRelativeToSourceRoot = (serverIsWindows ? path.win32 : path.posix).relative(
69-
serverSourceRoot,
70-
serverPath
71-
)
72-
if (serverIsWindows && !clientIsWindows) {
73-
pathRelativeToSourceRoot = pathRelativeToSourceRoot.replace(/\\/g, path.posix.sep)
74-
}
75-
if (clientIsWindows && /^[a-zA-Z]:$/.test(localSourceRoot)) {
76-
// if local source root mapping is only drive letter, add backslash
77-
localSourceRoot += '\\'
40+
if (serverSourceRootUrl && localSourceRootUrl) {
41+
fileUri = localSourceRootUrl + fileUri.substring(serverSourceRootUrl.length)
42+
}
43+
if (fileUri.startsWith('file://')) {
44+
const u = new URL(fileUri)
45+
let pathname = u.pathname
46+
if (isWindowsUri(fileUri)) {
47+
// From Node.js lib/internal/url.js pathToFileURL
48+
pathname = pathname.replace(/\//g, path.win32.sep)
49+
pathname = decodeURIComponent(pathname)
50+
if (u.hostname !== '') {
51+
localPath = `\\\\${url.domainToUnicode(u.hostname)}${pathname}`
52+
} else {
53+
localPath = pathname.slice(1)
54+
}
55+
} else {
56+
localPath = decodeURIComponent(pathname)
7857
}
79-
// resolve from the local source root
80-
localPath = (clientIsWindows ? path.win32 : path.posix).resolve(localSourceRoot, pathRelativeToSourceRoot)
8158
} else {
82-
localPath = (serverIsWindows ? path.win32 : path.posix).normalize(serverPath)
59+
// if it's not a file url it could be sshfs or something else
60+
localPath = fileUri
8361
}
8462
return localPath
8563
}
8664

8765
/** converts a local path from VS Code to a server-side Xdebug file URI with respect to source root settings */
8866
export function convertClientPathToDebugger(localPath: string, pathMapping?: { [index: string]: string }): string {
89-
let localSourceRoot: string | undefined
90-
let serverSourceRoot: string | undefined
91-
// Xdebug always lowercases Windows drive letters in file URIs
92-
const localFileUri = fileUrl(
93-
localPath.replace(/^[A-Z]:\\/, match => match.toLowerCase()),
94-
{ resolve: false }
95-
)
67+
let localSourceRootUrl: string | undefined
68+
let serverSourceRootUrl: string | undefined
69+
70+
// Parse or convert local path to URL
71+
const localFileUri = pathOrUrlToUrl(localPath)
72+
9673
let serverFileUri: string
9774
if (pathMapping) {
9875
for (const mappedServerPath of Object.keys(pathMapping)) {
99-
let mappedLocalSource = pathMapping[mappedServerPath]
100-
if (/^[a-zA-Z]:$/.test(mappedLocalSource)) {
101-
// if local source root mapping is only drive letter, add backslash
102-
mappedLocalSource += '\\'
76+
//let mappedLocalSource = pathMapping[mappedServerPath]
77+
let mappedLocalSourceUrl = pathOrUrlToUrl(pathMapping[mappedServerPath])
78+
// try exact match
79+
if (localFileUri.length === mappedLocalSourceUrl.length && isSameUri(localFileUri, mappedLocalSourceUrl)) {
80+
// bail early
81+
localSourceRootUrl = mappedLocalSourceUrl
82+
serverSourceRootUrl = pathOrUrlToUrl(mappedServerPath)
83+
break
84+
}
85+
// make sure it ends with a slash
86+
if (!mappedLocalSourceUrl.endsWith('/')) {
87+
mappedLocalSourceUrl += '/'
10388
}
104-
const localRelative = path.relative(mappedLocalSource, localPath)
105-
if (!localRelative.startsWith('..')) {
89+
90+
if (isSameUri(localFileUri.substring(0, mappedLocalSourceUrl.length), mappedLocalSourceUrl)) {
10691
// If a matching mapping has previously been found, only update
10792
// it if the current local path is longer than the previous one
10893
// (longest prefix matching)
109-
if (!localSourceRoot || mappedLocalSource.length > localSourceRoot.length) {
110-
serverSourceRoot = mappedServerPath
111-
localSourceRoot = mappedLocalSource
94+
if (!localSourceRootUrl || mappedLocalSourceUrl.length > localSourceRootUrl.length) {
95+
localSourceRootUrl = mappedLocalSourceUrl
96+
serverSourceRootUrl = pathOrUrlToUrl(mappedServerPath)
97+
if (!serverSourceRootUrl.endsWith('/')) {
98+
serverSourceRootUrl += '/'
99+
}
112100
}
113101
}
114102
}
115103
}
116-
if (localSourceRoot) {
117-
localSourceRoot = localSourceRoot.replace(/^[A-Z]:$/, match => match.toLowerCase())
118-
localSourceRoot = localSourceRoot.replace(/^[A-Z]:\\/, match => match.toLowerCase())
119-
localSourceRoot = localSourceRoot.replace(/^[A-Z]:\//, match => match.toLowerCase())
120-
}
121-
if (serverSourceRoot) {
122-
serverSourceRoot = serverSourceRoot.replace(/^[A-Z]:$/, match => match.toLowerCase())
123-
serverSourceRoot = serverSourceRoot.replace(/^[A-Z]:\\/, match => match.toLowerCase())
124-
serverSourceRoot = serverSourceRoot.replace(/^[A-Z]:\//, match => match.toLowerCase())
125-
}
126-
if (serverSourceRoot && localSourceRoot) {
127-
let localSourceRootUrl = fileUrl(localSourceRoot, { resolve: false })
128-
if (!localSourceRootUrl.endsWith('/')) {
129-
localSourceRootUrl += '/'
130-
}
131-
let serverSourceRootUrl = fileUrl(serverSourceRoot, { resolve: false })
132-
if (!serverSourceRootUrl.endsWith('/')) {
133-
serverSourceRootUrl += '/'
134-
}
135-
// get the part of the path that is relative to the source root
136-
const urlRelativeToSourceRoot = relativeUrl(localSourceRootUrl, localFileUri)
137-
// resolve from the server source root
138-
serverFileUri = url.resolve(serverSourceRootUrl, urlRelativeToSourceRoot)
104+
if (serverSourceRootUrl && localSourceRootUrl) {
105+
serverFileUri = serverSourceRootUrl + localFileUri.substring(localSourceRootUrl.length)
139106
} else {
140107
serverFileUri = localFileUri
141108
}
142109
return serverFileUri
143110
}
144111

145112
export function isWindowsUri(path: string): boolean {
146-
return /^file:\/\/\/[a-zA-Z]:\//.test(path)
113+
return /^file:\/\/\/[a-zA-Z]:\//.test(path) || /^file:\/\/[^/]/.test(path)
114+
}
115+
116+
function isWindowsPath(path: string): boolean {
117+
return /^[a-zA-Z]:\\/.test(path) || /^\\\\/.test(path) || /^[a-zA-Z]:$/.test(path) || /^[a-zA-Z]:\//.test(path)
118+
}
119+
120+
function pathOrUrlToUrl(path: string): string {
121+
// Do not try to parse windows drive letter paths
122+
if (!isWindowsPath(path)) {
123+
try {
124+
// try to parse, but do not modify
125+
new URL(path).toString()
126+
return path
127+
} catch (ex) {
128+
// should be a path
129+
}
130+
}
131+
// Not a URL, do some windows path mangling before it is converted to URL
132+
if (path.startsWith('\\\\')) {
133+
// UNC
134+
const hostEndIndex = path.indexOf('\\', 2)
135+
const host = path.substring(2, hostEndIndex)
136+
const outURL = new URL('file://')
137+
outURL.hostname = url.domainToASCII(host)
138+
outURL.pathname = path.substring(hostEndIndex).replace(/\\/g, '/')
139+
return outURL.toString()
140+
}
141+
if (/^[a-zA-Z]:$/.test(path)) {
142+
// if local source root mapping is only drive letter, add backslash
143+
path += '\\'
144+
}
145+
// Do not change drive later to lower case anymore
146+
// if (/^[a-zA-Z]:/.test(path)) {
147+
// // Xdebug always lowercases Windows drive letters in file URIs
148+
// //path = path.replace(/^[A-Z]:/, match => match.toLowerCase())
149+
// }
150+
return fileUrl(path, { resolve: false })
147151
}
148152

149153
export function isSameUri(clientUri: string, debuggerUri: string): boolean {

src/phpDebug.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -953,7 +953,7 @@ class PhpDebugSession extends vscode.DebugSession {
953953
line++
954954
} else {
955955
// Xdebug paths are URIs, VS Code file paths
956-
const filePath = convertDebuggerPathToClient(urlObject, this._args.pathMappings)
956+
const filePath = convertDebuggerPathToClient(status.fileUri, this._args.pathMappings)
957957
// "Name" of the source and the actual file path
958958
source = { name: path.basename(filePath), path: filePath }
959959
}
@@ -992,7 +992,7 @@ class PhpDebugSession extends vscode.DebugSession {
992992
line++
993993
} else {
994994
// Xdebug paths are URIs, VS Code file paths
995-
const filePath = convertDebuggerPathToClient(urlObject, this._args.pathMappings)
995+
const filePath = convertDebuggerPathToClient(stackFrame.fileUri, this._args.pathMappings)
996996
// "Name" of the source and the actual file path
997997
source = { name: path.basename(filePath), path: filePath }
998998
}

0 commit comments

Comments
 (0)