Skip to content

Commit 7b427a5

Browse files
committed
fix: Markdown LS returns unnormalized path for completions on windows
1 parent 9da5b7b commit 7b427a5

File tree

2 files changed

+67
-15
lines changed

2 files changed

+67
-15
lines changed

org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/markdown/MarkdownLanguageClient.java

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public void resourceChanged(final IResourceChangeEvent event) {
140140
// Notify server of the file system event for this resource
141141
final var payload = new HashMap<String, Object>();
142142
payload.put("id", Integer.valueOf(id));
143-
payload.put("uri", uri.toString());
143+
payload.put("uri", normalizeFileUriForLanguageServer(uri));
144144
payload.put("kind", kind);
145145
server.fsWatcherOnChange(payload);
146146
return true;
@@ -282,7 +282,7 @@ public CompletableFuture<List<String>> findMarkdownFilesInWorkspace(final Object
282282
if (res.getType() == IResource.FILE) {
283283
final String name = res.getName().toLowerCase();
284284
if (name.endsWith(".md") || name.endsWith(".markdown") || name.endsWith(".mdown")) {
285-
uris.add(res.getLocationURI().toString());
285+
uris.add(normalizeFileUriForLanguageServer(res.getLocationURI()));
286286
}
287287
return false; // no children
288288
}
@@ -298,7 +298,7 @@ public CompletableFuture<List<String>> findMarkdownFilesInWorkspace(final Object
298298
if (res.getType() == IResource.FILE) {
299299
final String name = res.getName().toLowerCase();
300300
if (name.endsWith(".md") || name.endsWith(".markdown") || name.endsWith(".mdown")) {
301-
uris.add(res.getLocationURI().toString());
301+
uris.add(normalizeFileUriForLanguageServer(res.getLocationURI()));
302302
}
303303
return false; // no children
304304
}
@@ -439,4 +439,35 @@ public CompletableFuture<Void> fsWatcherDelete(final Map<String, Object> params)
439439
return null;
440440
});
441441
}
442+
443+
/**
444+
* Normalize Windows file URIs to match vscode-uri's URI.toString() form used by the server.
445+
* <br>
446+
* Examples:
447+
* <li>file:/D:/path -> file:///d%3A/path
448+
* <li>file:///D:/path -> file:///d%3A/path
449+
*/
450+
private static String normalizeFileUriForLanguageServer(final URI uri) {
451+
if (uri == null)
452+
return null;
453+
if (!FileUtils.FILE_SCHEME.equalsIgnoreCase(uri.getScheme()))
454+
return uri.toString();
455+
final String uriAsString = uri.toString();
456+
457+
// Ensure triple slash prefix
458+
String withoutScheme = uriAsString.substring("file:".length()); // could be :/, :///
459+
while (withoutScheme.startsWith("/")) {
460+
withoutScheme = withoutScheme.substring(1);
461+
}
462+
463+
// Expect leading like D:/ or d:/ on Windows
464+
if (withoutScheme.length() >= 2 && Character.isLetter(withoutScheme.charAt(0)) && withoutScheme.charAt(1) == ':') {
465+
final char drive = Character.toLowerCase(withoutScheme.charAt(0));
466+
final String rest = withoutScheme.substring(2); // drop ':'
467+
return "file:///" + drive + "%3A" + (rest.startsWith("/") ? rest : "/" + rest);
468+
}
469+
470+
// Already in a normalized or UNC form; fall back to original
471+
return uriAsString;
472+
}
442473
}

org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/markdown/md-lsp-proxy.js

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@
3131
// server's `URI.toString()` form. This guarantees the server tracks the open document under
3232
// the key it uses during diagnostics, enabling `computeDiagnostics()` and `markdown/fs/*` requests.
3333
//
34+
// 3) Windows path suggestions (client→server) producing incorrect absolute drive paths:
35+
// Problem: When resolving workspace header/path completions, the current document URI and target
36+
// document URIs sometimes differ in Windows drive case/encoding (e.g., `file:///D:/...` vs
37+
// `file:///d%3A/...`). This causes relative path computation to fall back to odd absolute
38+
// paths such as `../../../../d:/...`.
39+
// Fix: Normalize the document URI in `textDocument/completion` requests so that the server sees a
40+
// consistent Windows-encoded form and computes clean relative paths (e.g., `GUIDE.md#...`).
41+
//
3442
// Note: On non-Windows URIs, normalization is a no-op; messages are forwarded unchanged.
3543

3644
const { spawn } = require('child_process');
@@ -45,7 +53,8 @@ const serverArgs = process.argv.slice(3);
4553
// Launch the wrapped language server; we proxy its stdio
4654
const child = spawn(process.execPath, [serverMain, ...serverArgs], { stdio: ['pipe', 'pipe', 'inherit'] });
4755

48-
// Client → Server (Problem 2): normalize Windows URIs and mirror lifecycle notifications
56+
// Client → Server (Problem 2 & 3): normalize Windows URIs, mirror lifecycle notifications,
57+
// and normalize completion requests
4958
let inBuffer = Buffer.alloc(0);
5059
process.stdin.on('data', chunk => {
5160
inBuffer = Buffer.concat([inBuffer, chunk]);
@@ -123,15 +132,14 @@ function processInboundBuffer() {
123132
}
124133
}
125134

126-
// Duplicate lifecycle notifications with a normalized URI so diagnostics can run (Problem 2)
135+
// Client → Server normalization (Problem 2 & 3)
127136
function transformInbound(bodyBuf) {
128137
try {
129138
const text = bodyBuf.toString('utf8');
130139
const msg = JSON.parse(text);
131140
const method = msg && msg.method;
132141

133-
// For text document lifecycle events, also send a duplicate event
134-
// with a normalized file URI so the server's URI.toString() lookups match.
142+
// Duplicate lifecycle notifications with a normalized URI (Problem 2)
135143
if ((method === 'textDocument/didOpen' || method === 'textDocument/didChange' || method === 'textDocument/didClose' || method === 'textDocument/willSave' || method === 'textDocument/didSave') && msg.params && msg.params.textDocument) {
136144
const origUri = msg.params.textDocument.uri;
137145
const normUri = normalizeFileUriForServer(origUri);
@@ -142,22 +150,35 @@ function transformInbound(bodyBuf) {
142150
return [Buffer.from(JSON.stringify(msg), 'utf8'), Buffer.from(JSON.stringify(dup), 'utf8')];
143151
}
144152
}
153+
154+
// Normalize completion requests so relative path suggestions are correct on Windows (Problem 3)
155+
if (method === 'textDocument/completion' && msg.params && msg.params.textDocument) {
156+
const origUri = msg.params.textDocument.uri;
157+
const normUri = normalizeFileUriForServer(origUri);
158+
if (normUri && normUri !== origUri) {
159+
const req = structuredClone(msg);
160+
req.params.textDocument.uri = normUri;
161+
return [Buffer.from(JSON.stringify(req), 'utf8')];
162+
}
163+
}
164+
145165
} catch { }
146166
return [bodyBuf];
147167
}
148168

149169
// Normalize Windows file URIs to match vscode-uri’s URI.toString() (Problem 2)
150170
function normalizeFileUriForServer(uri) {
151-
if (typeof uri !== 'string' || !uri.startsWith('file:///'))
152-
return undefined;
153-
154-
// Normalize Windows drive letter and encode colon to match vscode-uri toString()
155-
// Example: file:///D:/path -> file:///d%3A/path
156-
const after = uri.slice('file:///'.length);
171+
if (typeof uri !== 'string' || !uri.startsWith('file:')) return undefined;
172+
// Strip scheme and leading slashes to get to drive letter
173+
let after = uri.slice('file:'.length);
174+
while (after.startsWith('/')) after = after.slice(1);
175+
// Example accepted forms: D:/path or d:/path
157176
if (/^[A-Za-z]:/.test(after)) {
158177
const drive = after[0].toLowerCase();
159-
const rest = after.slice(2); // drop ":"
160-
return 'file:///' + drive + '%3A' + rest;
178+
const rest = after.slice(2); // drop ':'
179+
// Ensure a leading slash for the path segment
180+
const pathPart = rest.startsWith('/') ? rest : '/' + rest;
181+
return 'file:///' + drive + '%3A' + pathPart;
161182
}
162183
return undefined;
163184
}

0 commit comments

Comments
 (0)