Change:
Testing Version 1.21.0-insider Commit Date 2018-03-01T06:47:39.037Z create , in a folder in `' have and do code completion after . -should show as a proposal

Discussion:
onHtmlAttributeValue: ({ tag, attribute, value, range }) => { if (shouldDoPathCompletion(tag, attribute, value)) { <del> let workspaceRoot; if (startsWith(value, '/')) { if (!workspaceFolders || workspaceFolders.length === 0) { return; } workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders); </del> <ins> if (!workspaceFolders || workspaceFolders.length === 0) { return; </ins> } <ins> const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders); </ins> const suggestions = providePathSuggestions(value, range, URI.parse(document.uri).fsPath, workspaceRoot); result.items = [...suggestions, ...result.items];

} export function providePathSuggestions(value: string, range: Range, activeDocFsPath: string, root?: string): CompletionItem[] { <del> if (value.indexOf('/') === -1) { return []; } </del> if (startsWith(value, '/') && !root) { return []; } <ins> let replaceRange: Range; </ins> const lastIndexOfSlash = value.lastIndexOf('/'); <del> 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 []; </del> <ins> if (lastIndexOfSlash === -1) { replaceRange = getFullReplaceRange(range); } else { const valueAfterLastSlash = value.slice(lastIndexOfSlash + 1); replaceRange = getReplaceRange(range, valueAfterLastSlash); </ins> } <del> const replaceRange = getReplaceRange(range, valueAfterLastSlash); </del> <ins> 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); } </ins> try { return fs.readdirSync(parentDir).map(f => { <del> return { label: f, kind: isDir(path.resolve(parentDir, f)) ? CompletionItemKind.Folder : CompletionItemKind.File, textEdit: TextEdit.replace(replaceRange, f) }; </del> <ins> 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) }; } </ins> }); } catch (e) { return [];

} } <del> function getReplaceRange(valueRange: Range, valueAfterLastSlash: string): Range { </del> <ins> 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) { </ins> 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);

import * as assert from 'assert'; import * as path from 'path'; import { providePathSuggestions } from '../../modes/pathCompletion'; <del> import { CompletionItemKind, Range, Position } from 'vscode-languageserver-types'; </del> <ins> import { CompletionItemKind, Range, Position, CompletionItem, TextEdit, Command } from 'vscode-languageserver-types'; </ins> const fixtureRoot = path.resolve(__dirname, '../../../test/pathCompletionFixtures'); <del> suite('Path Completion - Relative Path', () => { const mockRange = Range.create(Position.create(0, 3), Position.create(0, 5)); </del> <ins> 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); </ins> test('Current Folder', () => { const value = './'; const activeFileFsPath = path.resolve(fixtureRoot, 'index.html'); const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath); <del> 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); </del> <ins> assertSuggestions(suggestions, [ { label: 'about/', kind: CompletionItemKind.Folder }, { label: 'index.html', kind: CompletionItemKind.File }, { label: 'src/', kind: CompletionItemKind.Folder } ]); </ins> }); <del> test('Parent Folder', () => { </del> <ins> test('Parent Folder:', () => { </ins> const value = '../'; const activeFileFsPath = path.resolve(fixtureRoot, 'about/about.html'); const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath); <del> 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); </del> <ins> assertSuggestions(suggestions, [ { label: 'about/', kind: CompletionItemKind.Folder }, { label: 'index.html', kind: CompletionItemKind.File }, { label: 'src/', kind: CompletionItemKind.Folder } ]); </ins> }); <del> test('Adjacent Folder', () => { </del> <ins> test('Adjacent Folder:', () => { </ins> const value = '../src/'; const activeFileFsPath = path.resolve(fixtureRoot, 'about/about.html'); const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath); <del> 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); </del> <ins> assertSuggestions(suggestions, [ { label: 'feature.js', kind: CompletionItemKind.File }, { label: 'test.js', kind: CompletionItemKind.File } ]); </ins> }); }); <del> suite('Path Completion - Absolute Path', () => { const mockRange = Range.create(Position.create(0, 3), Position.create(0, 5)); </del> <ins> suite('Path Completion - Absolute Path:', () => { const mockRange = toRange(0, 3, 5); </ins> test('Root', () => { const value = '/'; const activeFileFsPath1 = path.resolve(fixtureRoot, 'index.html'); const activeFileFsPath2 = path.resolve(fixtureRoot, 'about/index.html'); <del> const suggestions1 = providePathSuggestions(value, mockRange, activeFileFsPath1, fixtureRoot); const suggestions2 = providePathSuggestions(value, mockRange, activeFileFsPath2, fixtureRoot); </del> <ins> const suggestions1 = providePathSuggestions(value, mockRange, activeFileFsPath1, fixtureRoot); const suggestions2 = providePathSuggestions(value, mockRange, activeFileFsPath2, fixtureRoot); </ins> const verify = (suggestions) => { <del> 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); </del> <ins> assertSuggestions(suggestions, [ { label: 'about/', kind: CompletionItemKind.Folder }, { label: 'index.html', kind: CompletionItemKind.File }, { label: 'src/', kind: CompletionItemKind.Folder } ]); </ins> }; verify(suggestions1);

const activeFileFsPath = path.resolve(fixtureRoot, 'about/about.html'); const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath, fixtureRoot); <del> assert.equal(suggestions.length, 2); assert.equal(suggestions[0].label, 'feature.js'); assert.equal(suggestions[1].label, 'test.js'); </del> <ins> 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); </ins> <del> assert.equal(suggestions[0].kind, CompletionItemKind.File); assert.equal(suggestions[1].kind, CompletionItemKind.File); </del> <ins> assertSuggestions(suggestions, [ { label: 'about/', command: { title: 'Suggest', command: 'editor.action.triggerSuggest'} }, { label: 'index.html' }, { label: 'src/', command: { title: 'Suggest', command: 'editor.action.triggerSuggest'} }, ]); </ins> }); }); <del> suite('Path Completion - Incomplete Path at End', () => { const mockRange = Range.create(Position.create(0, 3), Position.create(0, 5)); </del> <ins> suite('Path Completion - Incomplete Path at End:', () => { const mockRange = toRange(0, 3, 5); </ins> 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); <del> 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); }); </del> <ins> assertSuggestions(suggestions, [ { label: 'feature.js', kind: CompletionItemKind.File }, { label: 'test.js', kind: CompletionItemKind.File } ]); }); </ins> 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); <del> assert.equal(suggestions.length, 2); assert.equal(suggestions[0].label, 'feature.js'); assert.equal(suggestions[1].label, 'test.js'); </del> <ins> 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); </ins> <del> assert.equal(suggestions[0].kind, CompletionItemKind.File); assert.equal(suggestions[1].kind, CompletionItemKind.File); }); </del> <ins> 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') } ]); }); </ins> }); <del> suite('Path Completion - TextEdit', () => { </del> <ins> suite('Path Completion - TextEdit:', () => { </ins> test('TextEdit has correct replace text and range', () => { const value = './'; const activeFileFsPath = path.resolve(fixtureRoot, 'index.html'); <del> 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); </del> <ins> 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/') }, ]); </ins> }); });