diff --git a/extensions/css-language-features/.vscode/launch.json b/extensions/css-language-features/.vscode/launch.json index 68f7c70e450..43e9880333e 100644 --- a/extensions/css-language-features/.vscode/launch.json +++ b/extensions/css-language-features/.vscode/launch.json @@ -17,8 +17,8 @@ ], "stopOnEntry": false, "sourceMaps": true, - "outFiles": ["${workspaceFolder}/client/out/**/*.js"], - "preLaunchTask": "npm" + "outFiles": ["${workspaceFolder}/client/out/**/*.js"] + // "preLaunchTask": "npm" }, { "name": "Launch Tests", diff --git a/extensions/css-language-features/server/src/cssServerMain.ts b/extensions/css-language-features/server/src/cssServerMain.ts index aba96f5ff6a..77eedcb624d 100644 --- a/extensions/css-language-features/server/src/cssServerMain.ts +++ b/extensions/css-language-features/server/src/cssServerMain.ts @@ -49,7 +49,7 @@ connection.onShutdown(() => { }); let scopedSettingsSupport = false; -let workspaceFolders: WorkspaceFolder[] | undefined; +let workspaceFolders: WorkspaceFolder[]; // After the server has started the client sends an initilize request. The server receives // in the passed params the rootPath of the workspace plus the client capabilities. diff --git a/extensions/css-language-features/server/src/pathCompletion.ts b/extensions/css-language-features/server/src/pathCompletion.ts index 1f8ddfc2df1..9bf9df83e5e 100644 --- a/extensions/css-language-features/server/src/pathCompletion.ts +++ b/extensions/css-language-features/server/src/pathCompletion.ts @@ -10,75 +10,67 @@ import URI from 'vscode-uri'; import { TextDocument, CompletionList, CompletionItemKind, CompletionItem, TextEdit, Range, Position } from 'vscode-languageserver-types'; import { WorkspaceFolder } from 'vscode-languageserver'; -import { ICompletionParticipant, URILiteralCompletionContext } from 'vscode-css-languageservice'; +import { ICompletionParticipant } from 'vscode-css-languageservice'; import { startsWith } from './utils/strings'; export function getPathCompletionParticipant( document: TextDocument, - workspaceFolders: WorkspaceFolder[] | undefined, + workspaceFolders: WorkspaceFolder[], result: CompletionList ): ICompletionParticipant { return { - onURILiteralValue: (context: URILiteralCompletionContext) => { - if (!workspaceFolders || workspaceFolders.length === 0) { - return; - } - const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders); + onURILiteralValue: ({ position, range, uriValue }) => { + const fullValue = stripQuotes(uriValue); - // Handle quoted values - let uriValue = context.uriValue; - let range = context.range; - if (startsWith(uriValue, `'`) || startsWith(uriValue, `"`)) { - uriValue = uriValue.slice(1, -1); - range = getRangeWithoutQuotes(range); - } + if (shouldDoPathCompletion(fullValue)) { + if (!workspaceFolders || workspaceFolders.length === 0) { + return; + } + const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders); - const suggestions = providePathSuggestions(uriValue, range, URI.parse(document.uri).fsPath, workspaceRoot); - result.items = [...suggestions, ...result.items]; + const paths = providePaths(fullValue, URI.parse(document.uri).fsPath, workspaceRoot); + result.items = [...paths.map(p => pathToSuggestion(p, fullValue, fullValue, range)), ...result.items]; + } } }; } -export function providePathSuggestions(value: string, range: Range, activeDocFsPath: string, root?: string): CompletionItem[] { - if (startsWith(value, '/') && !root) { +function stripQuotes(fullValue: string) { + if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) { + return fullValue.slice(1, -1); + } else { + return fullValue; + } +} + +function shouldDoPathCompletion(fullValue: string) { + if (fullValue === '.') { + return false; + } + return true; +} + +/** + * Get a list of path suggestions. Folder suggestions are suffixed with a slash. + */ +function providePaths(valueBeforeCursor: string, activeDocFsPath: string, root?: string): string[] { + if (startsWith(valueBeforeCursor, '/') && !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); - } + const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/'); + const valueBeforeLastSlash = valueBeforeCursor.slice(0, lastIndexOfSlash + 1); - const valueBeforeLastSlash = value.slice(0, lastIndexOfSlash + 1); - - const parentDir = startsWith(value, '/') + const parentDir = startsWith(valueBeforeCursor, '/') ? 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) - }; - } + return isDir(path.resolve(parentDir, f)) + ? f + '/' + : f; }); } catch (e) { return []; @@ -93,6 +85,48 @@ const isDir = (p: string) => { } }; +function pathToSuggestion(p: string, valueBeforeCursor: string, fullValue: string, range: Range): CompletionItem { + const isDir = p[p.length - 1] === '/'; + + let replaceRange: Range; + const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/'); + if (lastIndexOfSlash === -1) { + replaceRange = shiftRange(range, 1, -1); + } else { + // For cases where cursor is in the middle of attribute value, like