-
Notifications
You must be signed in to change notification settings - Fork 29.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #45788 from Microsoft/octref/pathCompletion
path completion for css. fix #45235
- Loading branch information
Showing
12 changed files
with
256 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
'use strict'; | ||
|
||
import * as path from 'path'; | ||
import * as fs from 'fs'; | ||
import URI from 'vscode-uri'; | ||
|
||
import { TextDocument, CompletionList, CompletionItemKind, CompletionItem, TextEdit, Range, Position } from 'vscode-languageserver-types'; | ||
import { WorkspaceFolder } from 'vscode-languageserver'; | ||
import { ICompletionParticipant } from 'vscode-css-languageservice'; | ||
|
||
import { startsWith } from './utils/strings'; | ||
|
||
export function getPathCompletionParticipant( | ||
document: TextDocument, | ||
workspaceFolders: WorkspaceFolder[] | undefined, | ||
result: CompletionList | ||
): ICompletionParticipant { | ||
return { | ||
onURILiteralValue: (context: { uriValue: string, position: Position, range: Range; }) => { | ||
if (!workspaceFolders || workspaceFolders.length === 0) { | ||
return; | ||
} | ||
const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders); | ||
|
||
// Handle quoted values | ||
let uriValue = context.uriValue; | ||
let range = context.range; | ||
if (startsWith(uriValue, `'`) || startsWith(uriValue, `"`)) { | ||
uriValue = uriValue.slice(1, -1); | ||
range = getRangeWithoutQuotes(range); | ||
} | ||
|
||
const suggestions = providePathSuggestions(uriValue, range, URI.parse(document.uri).fsPath, workspaceRoot); | ||
result.items = [...suggestions, ...result.items]; | ||
} | ||
}; | ||
} | ||
|
||
export function providePathSuggestions(value: string, range: Range, activeDocFsPath: string, root?: string): CompletionItem[] { | ||
if (startsWith(value, '/') && !root) { | ||
return []; | ||
} | ||
|
||
let replaceRange: Range; | ||
const lastIndexOfSlash = value.lastIndexOf('/'); | ||
if (lastIndexOfSlash === -1) { | ||
replaceRange = getFullReplaceRange(range); | ||
} else { | ||
const valueAfterLastSlash = value.slice(lastIndexOfSlash + 1); | ||
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 => { | ||
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 []; | ||
} | ||
} | ||
|
||
const isDir = (p: string) => { | ||
return fs.statSync(p).isDirectory(); | ||
}; | ||
|
||
function resolveWorkspaceRoot(activeDoc: TextDocument, workspaceFolders: WorkspaceFolder[]): string | undefined { | ||
for (let i = 0; i < workspaceFolders.length; i++) { | ||
if (startsWith(activeDoc.uri, workspaceFolders[i].uri)) { | ||
return path.resolve(URI.parse(workspaceFolders[i].uri).fsPath); | ||
} | ||
} | ||
} | ||
|
||
function getFullReplaceRange(valueRange: Range) { | ||
const start = Position.create(valueRange.end.line, valueRange.start.character); | ||
const end = Position.create(valueRange.end.line, valueRange.end.character); | ||
return Range.create(start, end); | ||
} | ||
function getReplaceRange(valueRange: Range, valueAfterLastSlash: string) { | ||
const start = Position.create(valueRange.end.line, valueRange.end.character - valueAfterLastSlash.length); | ||
const end = Position.create(valueRange.end.line, valueRange.end.character); | ||
return Range.create(start, end); | ||
} | ||
function getRangeWithoutQuotes(range: Range) { | ||
const start = Position.create(range.start.line, range.start.character + 1); | ||
const end = Position.create(range.end.line, range.end.character - 1); | ||
return Range.create(start, end); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
'use strict'; | ||
|
||
import 'mocha'; | ||
import * as assert from 'assert'; | ||
import * as path from 'path'; | ||
import Uri from 'vscode-uri'; | ||
import { TextDocument, CompletionList } from 'vscode-languageserver-types'; | ||
import { WorkspaceFolder } from 'vscode-languageserver-protocol'; | ||
import { getPathCompletionParticipant } from '../pathCompletion'; | ||
import { getCSSLanguageService } from 'vscode-css-languageservice'; | ||
|
||
export interface ItemDescription { | ||
label: string; | ||
resultText?: string; | ||
} | ||
|
||
suite('Completions', () => { | ||
const cssLanguageService = getCSSLanguageService(); | ||
|
||
let assertCompletion = function (completions: CompletionList, expected: ItemDescription, document: TextDocument, offset: number) { | ||
let matches = completions.items.filter(completion => { | ||
return completion.label === expected.label; | ||
}); | ||
|
||
assert.equal(matches.length, 1, `${expected.label} should only existing once: Actual: ${completions.items.map(c => c.label).join(', ')}`); | ||
let match = matches[0]; | ||
if (expected.resultText && match.textEdit) { | ||
assert.equal(TextDocument.applyEdits(document, [match.textEdit]), expected.resultText); | ||
} | ||
}; | ||
|
||
function assertCompletions(value: string, expected: { count?: number, items?: ItemDescription[] }, testUri: string, workspaceFolders?: WorkspaceFolder[]): void { | ||
const offset = value.indexOf('|'); | ||
value = value.substr(0, offset) + value.substr(offset + 1); | ||
|
||
const document = TextDocument.create(testUri, 'css', 0, value); | ||
const position = document.positionAt(offset); | ||
|
||
if (!workspaceFolders) { | ||
workspaceFolders = [{ name: 'x', uri: path.dirname(testUri) }]; | ||
} | ||
|
||
let participantResult = CompletionList.create([]); | ||
cssLanguageService.setCompletionParticipants([getPathCompletionParticipant(document, workspaceFolders, participantResult)]); | ||
|
||
const stylesheet = cssLanguageService.parseStylesheet(document); | ||
let list = cssLanguageService.doComplete!(document, position, stylesheet); | ||
list.items = list.items.concat(participantResult.items); | ||
|
||
if (expected.count) { | ||
assert.equal(list.items.length, expected.count); | ||
} | ||
if (expected.items) { | ||
for (let item of expected.items) { | ||
assertCompletion(list, item, document, offset); | ||
} | ||
} | ||
} | ||
|
||
test('CSS Path completion', function () { | ||
let testUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/about/about.css')).fsPath; | ||
|
||
assertCompletions('html { background-image: url("./|")', { | ||
items: [ | ||
{ label: 'about.html', resultText: 'html { background-image: url("./about.html")' } | ||
] | ||
}, testUri); | ||
|
||
assertCompletions(`html { background-image: url('../|')`, { | ||
items: [ | ||
{ label: 'about/', resultText: `html { background-image: url('../about/')` }, | ||
{ label: 'index.html', resultText: `html { background-image: url('../index.html')` }, | ||
{ label: 'src/', resultText: `html { background-image: url('../src/')` } | ||
] | ||
}, testUri); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
'use strict'; | ||
|
||
export function startsWith(haystack: string, needle: string): boolean { | ||
if (haystack.length < needle.length) { | ||
return false; | ||
} | ||
|
||
for (let i = 0; i < needle.length; i++) { | ||
if (haystack[i] !== needle[i]) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} |
4 changes: 4 additions & 0 deletions
4
extensions/css/server/test/pathCompletionFixtures/about/about.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ |
Empty file.
Empty file.
4 changes: 4 additions & 0 deletions
4
extensions/css/server/test/pathCompletionFixtures/src/feature.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ |
4 changes: 4 additions & 0 deletions
4
extensions/css/server/test/pathCompletionFixtures/src/test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters