Skip to content

Commit a5cc44e

Browse files
committed
Fix #44814
1 parent a5f49f5 commit a5cc44e

File tree

2 files changed

+65
-23
lines changed

2 files changed

+65
-23
lines changed

extensions/html-language-features/server/src/modes/pathCompletion.ts

+40-21
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,32 @@ export function getPathCompletionParticipant(
1919
result: CompletionList
2020
): ICompletionParticipant {
2121
return {
22-
onHtmlAttributeValue: ({ tag, attribute, value, range }) => {
22+
onHtmlAttributeValue: ({ tag, position, attribute, value: valueBeforeCursor, range }) => {
23+
const fullValue = getFullValueWithoutQuotes(document, range);
2324

24-
if (shouldDoPathCompletion(tag, attribute, value)) {
25+
if (shouldDoPathCompletion(tag, attribute, fullValue)) {
2526
if (!workspaceFolders || workspaceFolders.length === 0) {
2627
return;
2728
}
2829
const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders);
2930

30-
const paths = providePaths(value, URI.parse(document.uri).fsPath, workspaceRoot);
31-
const suggestions = paths.map(p => pathToSuggestion(p, value, range));
31+
const paths = providePaths(valueBeforeCursor, URI.parse(document.uri).fsPath, workspaceRoot);
32+
const suggestions = paths.map(p => pathToSuggestion(p, valueBeforeCursor, fullValue, range));
3233
result.items = [...suggestions, ...result.items];
3334
}
3435
}
3536
};
3637
}
3738

39+
function getFullValueWithoutQuotes(document: TextDocument, range: Range) {
40+
const fullValue = document.getText(range);
41+
if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) {
42+
return fullValue.slice(1, -1);
43+
} else {
44+
return fullValue;
45+
}
46+
}
47+
3848
function shouldDoPathCompletion(tag: string, attr: string, value: string): boolean {
3949
if (startsWith(value, 'http') || startsWith(value, 'https') || startsWith(value, '//')) {
4050
return false;
@@ -54,19 +64,19 @@ function shouldDoPathCompletion(tag: string, attr: string, value: string): boole
5464
/**
5565
* Get a list of path suggestions. Folder suggestions are suffixed with a slash.
5666
*/
57-
function providePaths(value: string, activeDocFsPath: string, root?: string): string[] {
58-
if (startsWith(value, '/') && !root) {
67+
function providePaths(valueBeforeCursor: string, activeDocFsPath: string, root?: string): string[] {
68+
if (startsWith(valueBeforeCursor, '/') && !root) {
5969
return [];
6070
}
6171

62-
const lastIndexOfSlash = value.lastIndexOf('/');
72+
const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/');
6373
let parentDir: string;
6474
if (lastIndexOfSlash === -1) {
6575
parentDir = path.resolve(root);
6676
} else {
67-
const valueBeforeLastSlash = value.slice(0, lastIndexOfSlash + 1);
77+
const valueBeforeLastSlash = valueBeforeCursor.slice(0, lastIndexOfSlash + 1);
6878

69-
parentDir = startsWith(value, '/')
79+
parentDir = startsWith(valueBeforeCursor, '/')
7080
? path.resolve(root, '.' + valueBeforeLastSlash)
7181
: path.resolve(activeDocFsPath, '..', valueBeforeLastSlash);
7282
}
@@ -82,16 +92,27 @@ function providePaths(value: string, activeDocFsPath: string, root?: string): st
8292
}
8393
}
8494

85-
function pathToSuggestion(p: string, value: string, range: Range): CompletionItem {
95+
function pathToSuggestion(p: string, valueBeforeCursor: string, fullValue: string, range: Range): CompletionItem {
8696
const isDir = p[p.length - 1] === '/';
8797

8898
let replaceRange: Range;
89-
const lastIndexOfSlash = value.lastIndexOf('/');
99+
const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/');
90100
if (lastIndexOfSlash === -1) {
91-
replaceRange = getFullReplaceRange(range);
101+
replaceRange = shiftRange(range, 1, -1);
92102
} else {
93-
const valueAfterLastSlash = value.slice(lastIndexOfSlash + 1);
94-
replaceRange = getReplaceRange(range, valueAfterLastSlash);
103+
// For cases where cursor is in the middle of attribute value, like <script src="./s|rc/test.js">
104+
// Find the last slash before cursor, and calculate the start of replace range from there
105+
const valueAfterLastSlash = fullValue.slice(lastIndexOfSlash + 1);
106+
const startPos = shiftPosition(range.end, -1 - valueAfterLastSlash.length);
107+
// If whitespace exists, replace until it
108+
const whiteSpaceIndex = valueAfterLastSlash.indexOf(' ');
109+
let endPos;
110+
if (whiteSpaceIndex !== -1) {
111+
endPos = shiftPosition(startPos, whiteSpaceIndex);
112+
} else {
113+
endPos = shiftPosition(range.end, -1);
114+
}
115+
replaceRange = Range.create(startPos, endPos);
95116
}
96117

97118
if (isDir) {
@@ -121,14 +142,12 @@ function resolveWorkspaceRoot(activeDoc: TextDocument, workspaceFolders: Workspa
121142
}
122143
}
123144

124-
function getFullReplaceRange(valueRange: Range) {
125-
const start = Position.create(valueRange.end.line, valueRange.start.character + 1);
126-
const end = Position.create(valueRange.end.line, valueRange.end.character - 1);
127-
return Range.create(start, end);
145+
function shiftPosition(pos: Position, offset: number): Position {
146+
return Position.create(pos.line, pos.character + offset);
128147
}
129-
function getReplaceRange(valueRange: Range, valueAfterLastSlash: string) {
130-
const start = Position.create(valueRange.end.line, valueRange.end.character - 1 - valueAfterLastSlash.length);
131-
const end = Position.create(valueRange.end.line, valueRange.end.character - 1);
148+
function shiftRange(range: Range, startOffset: number, endOffset: number): Range {
149+
const start = shiftPosition(range.start, startOffset);
150+
const end = shiftPosition(range.end, endOffset);
132151
return Range.create(start, end);
133152
}
134153

extensions/html-language-features/server/src/test/completions.test.ts

+25-2
Original file line numberDiff line numberDiff line change
@@ -217,15 +217,38 @@ suite('HTML Path Completion', () => {
217217
});
218218

219219
test('Trigger completion in middle of path', () => {
220-
/* Trigger Completion In Middle of path
221220
testCompletionFor('<script src="src/f|eature.js">', {
222221
items: [
223222
{ label: 'feature.js', resultText: '<script src="src/feature.js">' },
224223
{ label: 'test.js', resultText: '<script src="src/test.js">' },
225224
]
226225
}, indexHtmlUri, [fixtureWorkspace]);
227-
*/
228226

227+
testCompletionFor('<script src="s|rc/feature.js">', {
228+
items: [
229+
{ label: 'about/', resultText: '<script src="about/">' },
230+
{ label: 'index.html', resultText: '<script src="index.html">' },
231+
{ label: 'src/', resultText: '<script src="src/">' },
232+
]
233+
}, indexHtmlUri, [fixtureWorkspace]);
234+
});
235+
236+
test('Trigger completion in middle of path and with whitespaces', () => {
237+
testCompletionFor('<script src="./| about/about.html>', {
238+
items: [
239+
{ label: 'about/', resultText: '<script src="./about/ about/about.html>' },
240+
{ label: 'index.html', resultText: '<script src="./index.html about/about.html>' },
241+
{ label: 'src/', resultText: '<script src="./src/ about/about.html>' },
242+
]
243+
}, indexHtmlUri, [fixtureWorkspace]);
244+
245+
testCompletionFor('<script src="./a|bout /about.html>', {
246+
items: [
247+
{ label: 'about/', resultText: '<script src="./about/ /about.html>' },
248+
{ label: 'index.html', resultText: '<script src="./index.html /about.html>' },
249+
{ label: 'src/', resultText: '<script src="./src/ /about.html>' },
250+
]
251+
}, indexHtmlUri, [fixtureWorkspace]);
229252
});
230253

231254
test('Unquoted Path', () => {

0 commit comments

Comments
 (0)