未验证 提交 7618cf49 编写于 作者: P Pine 提交者: GitHub

Merge pull request #45437 from Microsoft/octref/pathCompletion

March path completion improvements
......@@ -22,15 +22,10 @@ export function getPathCompletionParticipant(
onHtmlAttributeValue: ({ tag, attribute, value, range }) => {
if (shouldDoPathCompletion(tag, attribute, value)) {
let workspaceRoot;
if (startsWith(value, '/')) {
if (!workspaceFolders || workspaceFolders.length === 0) {
return;
}
workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders);
if (!workspaceFolders || workspaceFolders.length === 0) {
return;
}
const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders);
const suggestions = providePathSuggestions(value, range, URI.parse(document.uri).fsPath, workspaceRoot);
result.items = [...suggestions, ...result.items];
......@@ -56,34 +51,49 @@ function shouldDoPathCompletion(tag: string, attr: string, value: string): boole
}
export function providePathSuggestions(value: string, range: Range, activeDocFsPath: string, root?: string): CompletionItem[] {
if (value.indexOf('/') === -1) {
return [];
}
if (startsWith(value, '/') && !root) {
return [];
}
let replaceRange: Range;
const lastIndexOfSlash = value.lastIndexOf('/');
const valueBeforeLastSlash = value.slice(0, lastIndexOfSlash + 1);
const valueAfterLastSlash = value.slice(lastIndexOfSlash + 1);
const parentDir = startsWith(value, '/')
? path.resolve(root, '.' + valueBeforeLastSlash)
: path.resolve(activeDocFsPath, '..', valueBeforeLastSlash);
if (!fs.existsSync(parentDir)) {
return [];
if (lastIndexOfSlash === -1) {
replaceRange = getFullReplaceRange(range);
} else {
const valueAfterLastSlash = value.slice(lastIndexOfSlash + 1);
replaceRange = getReplaceRange(range, valueAfterLastSlash);
}
const replaceRange = getReplaceRange(range, valueAfterLastSlash);
let parentDir: string;
if (lastIndexOfSlash === -1) {
parentDir = path.resolve(root);
} else {
const valueBeforeLastSlash = value.slice(0, lastIndexOfSlash + 1);
parentDir = startsWith(value, '/')
? path.resolve(root, '.' + valueBeforeLastSlash)
: path.resolve(activeDocFsPath, '..', valueBeforeLastSlash);
}
try {
return fs.readdirSync(parentDir).map(f => {
return {
label: f,
kind: isDir(path.resolve(parentDir, f)) ? CompletionItemKind.Folder : CompletionItemKind.File,
textEdit: TextEdit.replace(replaceRange, f)
};
if (isDir(path.resolve(parentDir, f))) {
return {
label: f + '/',
kind: CompletionItemKind.Folder,
textEdit: TextEdit.replace(replaceRange, f + '/'),
command: {
title: 'Suggest',
command: 'editor.action.triggerSuggest'
}
};
} else {
return {
label: f,
kind: CompletionItemKind.File,
textEdit: TextEdit.replace(replaceRange, f)
};
}
});
} catch (e) {
return [];
......@@ -102,7 +112,12 @@ function resolveWorkspaceRoot(activeDoc: TextDocument, workspaceFolders: Propose
}
}
function getReplaceRange(valueRange: Range, valueAfterLastSlash: string): Range {
function getFullReplaceRange(valueRange: Range) {
const start = Position.create(valueRange.end.line, valueRange.start.character + 1);
const end = Position.create(valueRange.end.line, valueRange.end.character - 1);
return Range.create(start, end);
}
function getReplaceRange(valueRange: Range, valueAfterLastSlash: string) {
const start = Position.create(valueRange.end.line, valueRange.end.character - 1 - valueAfterLastSlash.length);
const end = Position.create(valueRange.end.line, valueRange.end.character - 1);
return Range.create(start, end);
......
......@@ -7,78 +7,117 @@
import * as assert from 'assert';
import * as path from 'path';
import { providePathSuggestions } from '../../modes/pathCompletion';
import { CompletionItemKind, Range, Position } from 'vscode-languageserver-types';
import { CompletionItemKind, Range, Position, CompletionItem, TextEdit, Command } from 'vscode-languageserver-types';
const fixtureRoot = path.resolve(__dirname, '../../../test/pathCompletionFixtures');
suite('Path Completion - Relative Path', () => {
const mockRange = Range.create(Position.create(0, 3), Position.create(0, 5));
function toRange(line: number, startChar: number, endChar: number) {
return Range.create(Position.create(line, startChar), Position.create(line, endChar));
}
function toTextEdit(line: number, startChar: number, endChar: number, newText: string) {
const range = Range.create(Position.create(line, startChar), Position.create(line, endChar));
return TextEdit.replace(range, newText);
}
interface PathSuggestion {
label?: string;
kind?: CompletionItemKind;
textEdit?: TextEdit;
command?: Command;
}
function assertSuggestions(actual: CompletionItem[], expected: PathSuggestion[]) {
assert.equal(actual.length, expected.length, `Suggestions have length ${actual.length} but should have length ${expected.length}`);
for (let i = 0; i < expected.length; i++) {
if (expected[i].label) {
assert.equal(
actual[i].label,
expected[i].label,
`Suggestion ${actual[i].label} should have label ${expected[i].label}`
);
}
if (expected[i].kind) {
assert.equal(actual[i].kind,
expected[i].kind,
`Suggestion ${actual[i].label} has type ${CompletionItemKind[actual[i].kind]} but should have label ${CompletionItemKind[expected[i].kind]}`
);
}
if (expected[i].textEdit) {
assert.equal(actual[i].textEdit.newText, expected[i].textEdit.newText);
assert.deepEqual(actual[i].textEdit.range, expected[i].textEdit.range);
}
if (expected[i].command) {
assert.equal(
actual[i].command.title,
expected[i].command.title,
`Suggestion ${actual[i].label} has command title ${actual[i].command.title} but should have command title ${expected[i].command.title}`
);
assert.equal(
actual[i].command.command,
expected[i].command.command,
`Suggestion ${actual[i].label} has command ${actual[i].command.command} but should have command ${expected[i].command.command}`
);
}
}
}
suite('Path Completion - Relative Path:', () => {
const mockRange = toRange(0, 3, 5);
test('Current Folder', () => {
const value = './';
const activeFileFsPath = path.resolve(fixtureRoot, 'index.html');
const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath);
assert.equal(suggestions.length, 3);
assert.equal(suggestions[0].label, 'about');
assert.equal(suggestions[1].label, 'index.html');
assert.equal(suggestions[2].label, 'src');
assert.equal(suggestions[0].kind, CompletionItemKind.Folder);
assert.equal(suggestions[1].kind, CompletionItemKind.File);
assert.equal(suggestions[2].kind, CompletionItemKind.Folder);
assertSuggestions(suggestions, [
{ label: 'about/', kind: CompletionItemKind.Folder },
{ label: 'index.html', kind: CompletionItemKind.File },
{ label: 'src/', kind: CompletionItemKind.Folder }
]);
});
test('Parent Folder', () => {
test('Parent Folder:', () => {
const value = '../';
const activeFileFsPath = path.resolve(fixtureRoot, 'about/about.html');
const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath);
assert.equal(suggestions.length, 3);
assert.equal(suggestions[0].label, 'about');
assert.equal(suggestions[1].label, 'index.html');
assert.equal(suggestions[2].label, 'src');
assert.equal(suggestions[0].kind, CompletionItemKind.Folder);
assert.equal(suggestions[1].kind, CompletionItemKind.File);
assert.equal(suggestions[2].kind, CompletionItemKind.Folder);
assertSuggestions(suggestions, [
{ label: 'about/', kind: CompletionItemKind.Folder },
{ label: 'index.html', kind: CompletionItemKind.File },
{ label: 'src/', kind: CompletionItemKind.Folder }
]);
});
test('Adjacent Folder', () => {
test('Adjacent Folder:', () => {
const value = '../src/';
const activeFileFsPath = path.resolve(fixtureRoot, 'about/about.html');
const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath);
assert.equal(suggestions.length, 2);
assert.equal(suggestions[0].label, 'feature.js');
assert.equal(suggestions[1].label, 'test.js');
assert.equal(suggestions[0].kind, CompletionItemKind.File);
assert.equal(suggestions[1].kind, CompletionItemKind.File);
assertSuggestions(suggestions, [
{ label: 'feature.js', kind: CompletionItemKind.File },
{ label: 'test.js', kind: CompletionItemKind.File }
]);
});
});
suite('Path Completion - Absolute Path', () => {
const mockRange = Range.create(Position.create(0, 3), Position.create(0, 5));
suite('Path Completion - Absolute Path:', () => {
const mockRange = toRange(0, 3, 5);
test('Root', () => {
const value = '/';
const activeFileFsPath1 = path.resolve(fixtureRoot, 'index.html');
const activeFileFsPath2 = path.resolve(fixtureRoot, 'about/index.html');
const suggestions1 = providePathSuggestions(value, mockRange, activeFileFsPath1, fixtureRoot);
const suggestions2 = providePathSuggestions(value, mockRange, activeFileFsPath2, fixtureRoot);
const suggestions1 = providePathSuggestions(value, mockRange, activeFileFsPath1, fixtureRoot);
const suggestions2 = providePathSuggestions(value, mockRange, activeFileFsPath2, fixtureRoot);
const verify = (suggestions) => {
assert.equal(suggestions[0].label, 'about');
assert.equal(suggestions[1].label, 'index.html');
assert.equal(suggestions[2].label, 'src');
assert.equal(suggestions[0].kind, CompletionItemKind.Folder);
assert.equal(suggestions[1].kind, CompletionItemKind.File);
assert.equal(suggestions[2].kind, CompletionItemKind.Folder);
assertSuggestions(suggestions, [
{ label: 'about/', kind: CompletionItemKind.Folder },
{ label: 'index.html', kind: CompletionItemKind.File },
{ label: 'src/', kind: CompletionItemKind.Folder }
]);
};
verify(suggestions1);
......@@ -90,62 +129,109 @@ suite('Path Completion - Absolute Path', () => {
const activeFileFsPath = path.resolve(fixtureRoot, 'about/about.html');
const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath, fixtureRoot);
assert.equal(suggestions.length, 2);
assert.equal(suggestions[0].label, 'feature.js');
assert.equal(suggestions[1].label, 'test.js');
assertSuggestions(suggestions, [
{ label: 'feature.js', kind: CompletionItemKind.File },
{ label: 'test.js', kind: CompletionItemKind.File }
]);
});
});
suite('Path Completion - Folder Commands:', () => {
const mockRange = toRange(0, 3, 5);
test('Folder should have command `editor.action.triggerSuggest', () => {
const value = './';
const activeFileFsPath = path.resolve(fixtureRoot, 'index.html');
const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath);
assert.equal(suggestions[0].kind, CompletionItemKind.File);
assert.equal(suggestions[1].kind, CompletionItemKind.File);
assertSuggestions(suggestions, [
{ label: 'about/', command: { title: 'Suggest', command: 'editor.action.triggerSuggest'} },
{ label: 'index.html' },
{ label: 'src/', command: { title: 'Suggest', command: 'editor.action.triggerSuggest'} },
]);
});
});
suite('Path Completion - Incomplete Path at End', () => {
const mockRange = Range.create(Position.create(0, 3), Position.create(0, 5));
suite('Path Completion - Incomplete Path at End:', () => {
const mockRange = toRange(0, 3, 5);
test('Incomplete Path that starts with slash', () => {
const value = '/src/f';
const activeFileFsPath = path.resolve(fixtureRoot, 'about/about.html');
const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath, fixtureRoot);
assert.equal(suggestions.length, 2);
assert.equal(suggestions[0].label, 'feature.js');
assert.equal(suggestions[1].label, 'test.js');
assert.equal(suggestions[0].kind, CompletionItemKind.File);
assert.equal(suggestions[1].kind, CompletionItemKind.File);
});
assertSuggestions(suggestions, [
{ label: 'feature.js', kind: CompletionItemKind.File },
{ label: 'test.js', kind: CompletionItemKind.File }
]);
});
test('Incomplete Path that does not start with slash', () => {
const value = '../src/f';
const activeFileFsPath = path.resolve(fixtureRoot, 'about/about.html');
const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath, fixtureRoot);
assert.equal(suggestions.length, 2);
assert.equal(suggestions[0].label, 'feature.js');
assert.equal(suggestions[1].label, 'test.js');
assertSuggestions(suggestions, [
{ label: 'feature.js', kind: CompletionItemKind.File },
{ label: 'test.js', kind: CompletionItemKind.File }
]);
});
});
suite('Path Completion - No leading dot or slash:', () => {
test('Top level completion', () => {
const value = 's';
const activeFileFsPath = path.resolve(fixtureRoot, 'index.html');
const range = toRange(0, 3, 5);
const suggestions = providePathSuggestions(value, range, activeFileFsPath, fixtureRoot);
assertSuggestions(suggestions, [
{ label: 'about/', kind: CompletionItemKind.Folder, textEdit: toTextEdit(0, 4, 4, 'about/') },
{ label: 'index.html', kind: CompletionItemKind.File, textEdit: toTextEdit(0, 4, 4, 'index.html') },
{ label: 'src/', kind: CompletionItemKind.Folder, textEdit: toTextEdit(0, 4, 4, 'src/') }
]);
});
test('src/', () => {
const value = 'src/';
const activeFileFsPath = path.resolve(fixtureRoot, 'index.html');
const range = toRange(0, 3, 8);
const suggestions = providePathSuggestions(value, range, activeFileFsPath, fixtureRoot);
assert.equal(suggestions[0].kind, CompletionItemKind.File);
assert.equal(suggestions[1].kind, CompletionItemKind.File);
});
assertSuggestions(suggestions, [
{ label: 'feature.js', kind: CompletionItemKind.File, textEdit: toTextEdit(0, 7, 7, 'feature.js') },
{ label: 'test.js', kind: CompletionItemKind.File, textEdit: toTextEdit(0, 7, 7, 'test.js') }
]);
});
test('src/f', () => {
const value = 'src/f';
const activeFileFsPath = path.resolve(fixtureRoot, 'index.html');
const range = toRange(0, 3, 9);
const suggestions = providePathSuggestions(value, range, activeFileFsPath, fixtureRoot);
assertSuggestions(suggestions, [
{ label: 'feature.js', kind: CompletionItemKind.File, textEdit: toTextEdit(0, 7, 8, 'feature.js') },
{ label: 'test.js', kind: CompletionItemKind.File, textEdit: toTextEdit(0, 7, 8, 'test.js') }
]);
});
});
suite('Path Completion - TextEdit', () => {
suite('Path Completion - TextEdit:', () => {
test('TextEdit has correct replace text and range', () => {
const value = './';
const activeFileFsPath = path.resolve(fixtureRoot, 'index.html');
const range = Range.create(Position.create(0, 3), Position.create(0, 5));
const suggestions = providePathSuggestions(value, range, activeFileFsPath);
assert.equal(suggestions[0].textEdit.newText, 'about');
assert.equal(suggestions[1].textEdit.newText, 'index.html');
assert.equal(suggestions[2].textEdit.newText, 'src');
assert.equal(suggestions[0].textEdit.range.start.character, 4);
assert.equal(suggestions[1].textEdit.range.start.character, 4);
assert.equal(suggestions[2].textEdit.range.start.character, 4);
assert.equal(suggestions[0].textEdit.range.end.character, 4);
assert.equal(suggestions[1].textEdit.range.end.character, 4);
assert.equal(suggestions[2].textEdit.range.end.character, 4);
const range = toRange(0, 3, 5);
const expectedReplaceRange = toRange(0, 4, 4);
const suggestions = providePathSuggestions(value, range, activeFileFsPath);
assertSuggestions(suggestions, [
{ textEdit: TextEdit.replace(expectedReplaceRange, 'about/') },
{ textEdit: TextEdit.replace(expectedReplaceRange, 'index.html') },
{ textEdit: TextEdit.replace(expectedReplaceRange, 'src/') },
]);
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册