diff --git a/package.json b/package.json index 95c72d47..ff9526fa 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "color-name": "1.1.3", "fast-glob": "3.1.0", "scss-symbols-parser": "2.0.1", - "vscode-css-languageservice": "2.1.0", + "vscode-css-languageservice": "^4.0.3-next.23", "vscode-languageclient": "^5.2.1", "vscode-languageserver": "^5.2.1", "vscode-languageserver-types": "^3.14.0", diff --git a/src/parser/mixin.ts b/src/parser/mixin.ts deleted file mode 100644 index bb9aab92..00000000 --- a/src/parser/mixin.ts +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -import { INode, NodeType } from '../types/nodes'; -import { IVariable, IMixin } from '../types/symbols'; - -import { makeVariable } from './variable'; -import { getChildByType } from '../utils/ast'; - -/** - * Returns information about Mixin Declaraion. - */ -export function makeMixin(node: INode): IMixin { - const name = node.getName(); - const params: IVariable[] = []; - - node.getParameters() - .getChildren() - .forEach(child => { - if (child.getName()) { - params.push(makeVariable(child, name)); - } - }); - - return { - name, - parameters: params, - offset: node.offset - }; -} - -/** - * Returns information about set of Variable Declarations. - */ -export function makeMixinCollection(node: INode): IMixin[] { - return getChildByType(node, NodeType.MixinDeclaration).map(makeMixin); -} diff --git a/src/parser/symbols.ts b/src/parser/symbols.ts deleted file mode 100644 index 26e90ad0..00000000 --- a/src/parser/symbols.ts +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; - -import { IVariable, IMixin, IFunction, IImport, parseSymbols } from 'scss-symbols-parser'; - -import { INode, NodeType } from '../types/nodes'; -import { ISymbols } from '../types/symbols'; - -import { makeVariableCollection } from './variable'; -import { makeMixin, makeMixinCollection } from './mixin'; -import { getNodeAtOffset } from '../utils/ast'; - -/** - * Get all suggestions in file. - */ -export function findSymbols(text: string): ISymbols { - return parseSymbols(text); -} - -/** - * Get Symbols by offset position. - */ -export function findSymbolsAtOffset(parsedDocument: INode, offset: number): ISymbols { - let variables: IVariable[] = []; - let mixins: IMixin[] = []; - const functions: IFunction[] = []; - const imports: IImport[] = []; - - let node = getNodeAtOffset(parsedDocument, offset); - if (!node) { - return { - variables, - mixins, - functions, - imports - }; - } - - while (node && node.type !== NodeType.Stylesheet) { - if (node.type === NodeType.MixinDeclaration || node.type === NodeType.FunctionDeclaration) { - variables = variables.concat(makeMixin(node).parameters); - } else if (node.type === NodeType.Ruleset || node.type === NodeType.Declarations) { - variables = variables.concat(makeVariableCollection(node)); - mixins = mixins.concat(makeMixinCollection(node)); - } - - node = node.getParent(); - } - - return { - variables, - mixins, - functions, - imports - }; -} diff --git a/src/parser/variable.ts b/src/parser/variable.ts deleted file mode 100644 index d5d284f5..00000000 --- a/src/parser/variable.ts +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -import { INode, NodeType } from '../types/nodes'; -import { IVariable } from '../types/symbols'; - -import { getChildByType } from '../utils/ast'; - -/** - * Returns information about Variable Declaration. - */ -export function makeVariable(node: INode, fromMixin: string = null): IVariable { - const valueNode = fromMixin ? node.getDefaultValue() : node.getValue(); - - let value: string = null; - if (valueNode) { - value = valueNode - .getText() - .replace(/\n/g, ' ') - .replace(/\s\s+/g, ' '); - } - - return { - name: node.getName(), - value, - mixin: fromMixin, - offset: node.offset - }; -} - -/** - * Returns information about set of Variable Declarations. - */ -export function makeVariableCollection(node: INode): IVariable[] { - return getChildByType(node, NodeType.VariableDeclaration).map(child => { - return makeVariable(child, null); - }); -} diff --git a/src/providers/completion.ts b/src/providers/completion.ts index ea6becfc..91e6f412 100644 --- a/src/providers/completion.ts +++ b/src/providers/completion.ts @@ -1,15 +1,14 @@ 'use strict'; -import { CompletionList, CompletionItemKind, TextDocument, Files } from 'vscode-languageserver'; +import { CompletionList, CompletionItemKind, TextDocument, Files, CompletionItem } from 'vscode-languageserver'; -import { INode, NodeType } from '../types/nodes'; -import { IMixin } from '../types/symbols'; +import { IMixin, ISymbols } from '../types/symbols'; import { ISettings } from '../types/settings'; import StorageService from '../services/storage'; import { parseDocument } from '../services/parser'; -import { getSymbolsCollection } from '../utils/symbols'; -import { getCurrentDocumentImportPaths, getDocumentPath } from '../utils/document'; +import { getSymbolsRelatedToDocument } from '../utils/symbols'; +import { getDocumentPath } from '../utils/document'; import { getCurrentWord, getLimitedString, getTextBeforePosition } from '../utils/string'; import { getVariableColor } from '../utils/color'; @@ -36,27 +35,6 @@ function makeMixinDocumentation(symbol: IMixin): string { return `${symbol.name}(${args}) {\u2026}`; } -/** - * Skip suggestions for parent Mixin inside Mixins. - */ -function mixinSuggestionsFilter(mixin: IMixin, node: INode): boolean { - if (!node) { - return false; - } - - while (node.type !== NodeType.Stylesheet) { - if (node.type === NodeType.MixinDeclaration) { - const identifier = node.getIdentifier(); - if (identifier && identifier.getText() === mixin.name) { - return true; - } - } - node = node.getParent(); - } - - return false; -} - /** * Check context for Variables suggestions. */ @@ -104,6 +82,145 @@ function checkFunctionContext( return false; } +function isCommentContext(text: string): boolean { + return reComment.test(text.trim()); +} + +function isInterpolationContext(text: string): boolean { + return text.includes('#{'); +} + +function createCompletionContext(document: TextDocument, offset: number, settings: ISettings) { + const currentWord = getCurrentWord(document.getText(), offset); + const textBeforeWord = getTextBeforePosition(document.getText(), offset); + + // Is "#{INTERPOLATION}" + const isInterpolation = isInterpolationContext(currentWord); + + // Information about current position + const isPropertyValue = rePropertyValue.test(textBeforeWord); + const isEmptyValue = reEmptyPropertyValue.test(textBeforeWord); + const isQuotes = reQuotes.test(textBeforeWord.replace(reQuotedValueInString, '')); + + return { + comment: isCommentContext(textBeforeWord), + variable: checkVariableContext(currentWord, isInterpolation, isPropertyValue, isEmptyValue, isQuotes), + function: checkFunctionContext( + textBeforeWord, + isInterpolation, + isPropertyValue, + isEmptyValue, + isQuotes, + settings + ), + mixin: checkMixinContext(textBeforeWord, isPropertyValue) + }; +} + +function createVariableCompletionItems( + symbols: ISymbols[], + filepath: string, + imports: string[], + settings: ISettings +): CompletionItem[] { + const completions: CompletionItem[] = []; + + symbols.forEach(symbol => { + const isImplicitlyImport = isImplicitly(symbol.document, filepath, imports); + const fsPath = getDocumentPath(filepath, isImplicitlyImport ? symbol.filepath : symbol.document); + + symbol.variables.forEach(variable => { + const color = getVariableColor(variable.value); + const completionKind = color ? CompletionItemKind.Color : CompletionItemKind.Variable; + + // Add 'implicitly' prefix for Path if the file imported implicitly + let detailPath = fsPath; + if (isImplicitlyImport && settings.implicitlyLabel) { + detailPath = settings.implicitlyLabel + ' ' + detailPath; + } + + // Add 'argument from MIXIN_NAME' suffix if Variable is Mixin argument + let detailText = detailPath; + if (variable.mixin) { + detailText = `argument from ${variable.mixin}, ${detailText}`; + } + + completions.push({ + label: variable.name, + kind: completionKind, + detail: detailText, + documentation: getLimitedString(color ? color.toString() : variable.value) + }); + }); + }); + + return completions; +} + +function createMixinCompletionItems( + symbols: ISymbols[], + filepath: string, + imports: string[], + settings: ISettings +): CompletionItem[] { + const completions: CompletionItem[] = []; + + symbols.forEach(symbol => { + const isImplicitlyImport = isImplicitly(symbol.document, filepath, imports); + const fsPath = getDocumentPath(filepath, isImplicitlyImport ? symbol.filepath : symbol.document); + + symbol.mixins.forEach(mixin => { + // Add 'implicitly' prefix for Path if the file imported implicitly + let detailPath = fsPath; + if (isImplicitlyImport && settings.implicitlyLabel) { + detailPath = settings.implicitlyLabel + ' ' + detailPath; + } + + completions.push({ + label: mixin.name, + kind: CompletionItemKind.Function, + detail: detailPath, + documentation: makeMixinDocumentation(mixin), + insertText: mixin.name + }); + }); + }); + + return completions; +} + +function createFunctionCompletionItems( + symbols: ISymbols[], + filepath: string, + imports: string[], + settings: ISettings +): CompletionItem[] { + const completions: CompletionItem[] = []; + + symbols.forEach(symbol => { + const isImplicitlyImport = isImplicitly(symbol.document, filepath, imports); + const fsPath = getDocumentPath(filepath, isImplicitlyImport ? symbol.filepath : symbol.document); + + symbol.functions.forEach(func => { + // Add 'implicitly' prefix for Path if the file imported implicitly + let detailPath = fsPath; + if (isImplicitlyImport && settings.implicitlyLabel) { + detailPath = settings.implicitlyLabel + ' ' + detailPath; + } + + completions.push({ + label: func.name, + kind: CompletionItemKind.Interface, + detail: detailPath, + documentation: makeMixinDocumentation(func), + insertText: func.name + }); + }); + }); + + return completions; +} + /** * Do Completion :) */ @@ -120,128 +237,35 @@ export function doCompletion( return null; } - const resource = parseDocument(document, offset, settings); + const resource = parseDocument(document, offset); storage.set(documentPath, resource.symbols); - const symbolsList = getSymbolsCollection(storage); - const documentImports = getCurrentDocumentImportPaths(symbolsList, documentPath); - const currentWord = getCurrentWord(document.getText(), offset); - const textBeforeWord = getTextBeforePosition(document.getText(), offset); + const symbolsList = getSymbolsRelatedToDocument(storage, documentPath); + const documentImports = resource.symbols.imports.map(x => x.filepath); + const context = createCompletionContext(document, offset, settings); // Drop suggestions inside `//` and `/* */` comments - if (reComment.test(textBeforeWord.trim())) { + if (context.comment) { return completions; } - // Is "#{INTERPOLATION}" - const isInterpolation = currentWord.includes('#{'); - - // Information about current position - const isPropertyValue = rePropertyValue.test(textBeforeWord); - const isEmptyValue = reEmptyPropertyValue.test(textBeforeWord); - const isQuotes = reQuotes.test(textBeforeWord.replace(reQuotedValueInString, '')); + if (settings.suggestVariables && context.variable) { + const variables = createVariableCompletionItems(symbolsList, documentPath, documentImports, settings); - // Check contexts - const isVariableContext = checkVariableContext( - currentWord, - isInterpolation, - isPropertyValue, - isEmptyValue, - isQuotes - ); - const isFunctionContext = checkFunctionContext( - textBeforeWord, - isInterpolation, - isPropertyValue, - isEmptyValue, - isQuotes, - settings - ); - const isMixinContext = checkMixinContext(textBeforeWord, isPropertyValue); - - // Variables - if (settings.suggestVariables && isVariableContext) { - symbolsList.forEach(symbols => { - const isImplicitlyImport = isImplicitly(symbols.document, documentPath, documentImports); - const fsPath = getDocumentPath(documentPath, isImplicitlyImport ? symbols.filepath : symbols.document); - - symbols.variables.forEach(variable => { - const color = getVariableColor(variable.value); - const completionKind = color ? CompletionItemKind.Color : CompletionItemKind.Variable; - - // Add 'implicitly' prefix for Path if the file imported implicitly - let detailPath = fsPath; - if (isImplicitlyImport && settings.implicitlyLabel) { - detailPath = settings.implicitlyLabel + ' ' + detailPath; - } - - // Add 'argument from MIXIN_NAME' suffix if Variable is Mixin argument - let detailText = detailPath; - if (variable.mixin) { - detailText = `argument from ${variable.mixin}, ${detailText}`; - } - - completions.items.push({ - label: variable.name, - kind: completionKind, - detail: detailText, - documentation: getLimitedString(color ? color.toString() : variable.value) - }); - }); - }); + completions.items = completions.items.concat(variables); } - // Mixins - if (settings.suggestMixins && isMixinContext) { - symbolsList.forEach(symbols => { - const isImplicitlyImport = isImplicitly(symbols.document, documentPath, documentImports); - const fsPath = getDocumentPath(documentPath, isImplicitlyImport ? symbols.filepath : symbols.document); - - symbols.mixins.forEach(mixin => { - if (mixinSuggestionsFilter(mixin, resource.node)) { - return; - } - - // Add 'implicitly' prefix for Path if the file imported implicitly - let detailPath = fsPath; - if (isImplicitlyImport && settings.implicitlyLabel) { - detailPath = settings.implicitlyLabel + ' ' + detailPath; - } - - completions.items.push({ - label: mixin.name, - kind: CompletionItemKind.Function, - detail: detailPath, - documentation: makeMixinDocumentation(mixin), - insertText: mixin.name - }); - }); - }); + if (settings.suggestMixins && context.mixin) { + const mixins = createMixinCompletionItems(symbolsList, documentPath, documentImports, settings); + + completions.items = completions.items.concat(mixins); } - // Functions - if (settings.suggestFunctions && isFunctionContext) { - symbolsList.forEach(symbols => { - const isImplicitlyImport = isImplicitly(symbols.document, documentPath, documentImports); - const fsPath = getDocumentPath(documentPath, isImplicitlyImport ? symbols.filepath : symbols.document); - - symbols.functions.forEach(func => { - // Add 'implicitly' prefix for Path if the file imported implicitly - let detailPath = fsPath; - if (isImplicitlyImport && settings.implicitlyLabel) { - detailPath = settings.implicitlyLabel + ' ' + detailPath; - } - - completions.items.push({ - label: func.name, - kind: CompletionItemKind.Interface, - detail: detailPath, - documentation: makeMixinDocumentation(func), - insertText: func.name - }); - }); - }); + if (settings.suggestFunctions && context.function) { + const functions = createFunctionCompletionItems(symbolsList, documentPath, documentImports, settings); + + completions.items = completions.items.concat(functions); } return completions; diff --git a/src/providers/goDefinition.ts b/src/providers/goDefinition.ts index 0286d2e4..dafcff4b 100644 --- a/src/providers/goDefinition.ts +++ b/src/providers/goDefinition.ts @@ -5,11 +5,10 @@ import Uri from 'vscode-uri'; import { NodeType } from '../types/nodes'; import { ISymbols } from '../types/symbols'; -import { ISettings } from '../types/settings'; import StorageService from '../services/storage'; import { parseDocument } from '../services/parser'; -import { getSymbolsCollection } from '../utils/symbols'; +import { getSymbolsRelatedToDocument } from '../utils/symbols'; import { getDocumentPath } from '../utils/document'; interface ISymbol { @@ -54,18 +53,13 @@ function getSymbols(symbolList: ISymbols[], identifier: IIdentifier, currentPath /** * Do Go Definition :) */ -export function goDefinition( - document: TextDocument, - offset: number, - storage: StorageService, - settings: ISettings -): Promise { +export function goDefinition(document: TextDocument, offset: number, storage: StorageService): Promise { const documentPath = Files.uriToFilePath(document.uri) || document.uri; if (!documentPath) { return Promise.resolve(null); } - const resource = parseDocument(document, offset, settings); + const resource = parseDocument(document, offset); const hoverNode = resource.node; if (!hoverNode || !hoverNode.type) { return Promise.resolve(null); @@ -109,7 +103,7 @@ export function goDefinition( storage.set(resource.symbols.document, resource.symbols); - const symbolsList = getSymbolsCollection(storage); + const symbolsList = getSymbolsRelatedToDocument(storage, documentPath); // Symbols const candidates = getSymbols(symbolsList, identifier, documentPath); diff --git a/src/providers/hover.ts b/src/providers/hover.ts index d6a9f9c7..2c0df3f0 100644 --- a/src/providers/hover.ts +++ b/src/providers/hover.ts @@ -4,12 +4,11 @@ import { Hover, MarkedString, TextDocument, Files } from 'vscode-languageserver' import { NodeType } from '../types/nodes'; import { ISymbols, IVariable, IMixin, IFunction } from '../types/symbols'; -import { ISettings } from '../types/settings'; import StorageService from '../services/storage'; import { parseDocument } from '../services/parser'; import { getSymbolsCollection } from '../utils/symbols'; -import { getCurrentDocumentImportPaths, getDocumentPath } from '../utils/document'; +import { getDocumentPath } from '../utils/document'; import { getLimitedString } from '../utils/string'; /** @@ -92,16 +91,16 @@ function getSymbol(symbolList: ISymbols[], identifier: any, currentPath: string) /** * Do Hover :) */ -export function doHover(document: TextDocument, offset: number, storage: StorageService, settings: ISettings): Hover { +export function doHover(document: TextDocument, offset: number, storage: StorageService): Hover | null { const documentPath = Files.uriToFilePath(document.uri) || document.uri; if (!documentPath) { return null; } - const resource = parseDocument(document, offset, settings); + const resource = parseDocument(document, offset); const hoverNode = resource.node; if (!hoverNode || !hoverNode.type) { - return; + return null; } let identifier: { type: string; name: string } = null; @@ -141,13 +140,13 @@ export function doHover(document: TextDocument, offset: number, storage: Storage } if (!identifier) { - return; + return null; } storage.set(documentPath, resource.symbols); const symbolsList = getSymbolsCollection(storage); - const documentImports = getCurrentDocumentImportPaths(symbolsList, documentPath); + const documentImports = resource.symbols.imports.map(x => x.filepath); const symbol = getSymbol(symbolsList, identifier, documentPath); // Content for Hover popup diff --git a/src/providers/signatureHelp.ts b/src/providers/signatureHelp.ts index 4aba1b72..6051aa7e 100644 --- a/src/providers/signatureHelp.ts +++ b/src/providers/signatureHelp.ts @@ -4,7 +4,6 @@ import { SignatureHelp, SignatureInformation, TextDocument, Files } from 'vscode import { tokenizer } from 'scss-symbols-parser'; import { IVariable } from '../types/symbols'; -import { ISettings } from '../types/settings'; import StorageService from '../services/storage'; import { parseDocument } from '../services/parser'; @@ -141,12 +140,7 @@ function parseArgumentsAtLine(text: string): IMixinEntry { /** * Do Signature Help :) */ -export function doSignatureHelp( - document: TextDocument, - offset: number, - storage: StorageService, - settings: ISettings -): SignatureHelp { +export function doSignatureHelp(document: TextDocument, offset: number, storage: StorageService): SignatureHelp { const suggestions: { name: string; parameters: IVariable[] }[] = []; const ret: SignatureHelp = { @@ -173,7 +167,7 @@ export function doSignatureHelp( const symbolType = textBeforeWord.indexOf('@include') !== -1 ? 'mixins' : 'functions'; - const resource = parseDocument(document, offset, settings); + const resource = parseDocument(document, offset); storage.set(documentPath, resource.symbols); diff --git a/src/server.ts b/src/server.ts index 9b4193a9..050c3dd1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -99,19 +99,19 @@ connection.onCompletion(textDocumentPosition => { connection.onHover(textDocumentPosition => { const document = documents.get(textDocumentPosition.textDocument.uri); const offset = document.offsetAt(textDocumentPosition.position); - return doHover(document, offset, storageService, settings); + return doHover(document, offset, storageService); }); connection.onSignatureHelp(textDocumentPosition => { const document = documents.get(textDocumentPosition.textDocument.uri); const offset = document.offsetAt(textDocumentPosition.position); - return doSignatureHelp(document, offset, storageService, settings); + return doSignatureHelp(document, offset, storageService); }); connection.onDefinition(textDocumentPosition => { const document = documents.get(textDocumentPosition.textDocument.uri); const offset = document.offsetAt(textDocumentPosition.position); - return goDefinition(document, offset, storageService, settings); + return goDefinition(document, offset, storageService); }); connection.onWorkspaceSymbol(workspaceSymbolParams => { diff --git a/src/services/parser.ts b/src/services/parser.ts index 5748baa8..0db5c88b 100644 --- a/src/services/parser.ts +++ b/src/services/parser.ts @@ -3,36 +3,35 @@ import * as path from 'path'; import { TextDocument, Files } from 'vscode-languageserver'; -import { getSCSSLanguageService } from 'vscode-css-languageservice'; +import { getSCSSLanguageService, SymbolKind, DocumentLink } from 'vscode-css-languageservice'; -import { INode } from '../types/nodes'; -import { IDocument, ISymbols } from '../types/symbols'; -import { ISettings } from '../types/settings'; - -import { findSymbols, findSymbolsAtOffset } from '../parser/symbols'; -import { getNodeAtOffset } from '../utils/ast'; +import { INode, NodeType } from '../types/nodes'; +import { IDocument, ISymbols, IVariable, IImport } from '../types/symbols'; +import { getNodeAtOffset, getParentNodeByType } from '../utils/ast'; // RegExp's const reReferenceCommentGlobal = /\/\/\s*/g; const reReferenceComment = /\/\/\s*/; -const reDynamicPath = /#{}\*/; +const reDynamicPath = /[#{}\*]/; // SCSS Language Service const ls = getSCSSLanguageService(); ls.configure({ - lint: false, validate: false }); /** * Returns all Symbols in a single document. */ -export function parseDocument(document: TextDocument, offset: number = null, settings: ISettings): IDocument { +export function parseDocument(document: TextDocument, offset: number = null): IDocument { + const ast = ls.parseStylesheet(document) as INode; + const documentPath = Files.uriToFilePath(document.uri) || document.uri; + const symbols: ISymbols = { - ...getDocumentSymbols(document, settings), - document: Files.uriToFilePath(document.uri) || document.uri, - filepath: Files.uriToFilePath(document.uri) || document.uri + document: documentPath, + filepath: documentPath, + ...findDocumentSymbols(document, ast) }; // Get ` comments from document @@ -41,67 +40,124 @@ export function parseDocument(document: TextDocument, offset: number = null, set references.forEach(x => { const filepath = reReferenceComment.exec(x)[1]; symbols.imports.push({ - css: filepath.substr(-4) === '.css', + css: filepath.endsWith('.css'), dynamic: reDynamicPath.test(filepath), - filepath, + filepath: resolveReference(filepath, documentPath), reference: true }); }); } - let ast: INode = null; - if (offset) { - ast = ls.parseStylesheet(document) as INode; + return { + node: getNodeAtOffset(ast, offset), + symbols + }; +} + +function findDocumentSymbols(document: TextDocument, ast: INode): ISymbols { + const symbols = ls.findDocumentSymbols(document, ast); + const links = findDocumentLinks(document, ast); - const scopedSymbols = findSymbolsAtOffset(ast, offset); + const result: ISymbols = { + functions: [], + imports: convertLinksToImports(links), + mixins: [], + variables: [] + }; - symbols.variables = symbols.variables.concat(scopedSymbols.variables); - symbols.mixins = symbols.mixins.concat(scopedSymbols.mixins); - } + for (const symbol of symbols) { + const position = symbol.location.range.start; + const offset = document.offsetAt(symbol.location.range.start); - symbols.imports = symbols.imports.map(x => { - if (x.filepath[0] === '~') { - x.filepath = 'node_modules/' + x.filepath.slice(1); - } - x.filepath = path.join(path.dirname(symbols.document), x.filepath); - if (!x.css && x.filepath.substr(-5) !== '.scss') { - x.filepath += '.scss'; + if (symbol.kind === SymbolKind.Variable) { + result.variables.push({ + name: symbol.name, + offset, + position, + value: getVariableValue(ast, offset) + }); + } else if (symbol.kind === SymbolKind.Method) { + result.mixins.push({ + name: symbol.name, + offset, + position, + parameters: getMethodParameters(ast, offset) + }); + } else if (symbol.kind === SymbolKind.Function) { + result.functions.push({ + name: symbol.name, + offset, + position, + parameters: getMethodParameters(ast, offset) + }); } - return x; - }); + } - symbols.variables = symbols.variables.map(x => { - x.position = document.positionAt(x.offset); - return x; - }); - symbols.mixins = symbols.mixins.map(x => { - x.position = document.positionAt(x.offset); - return x; - }); - symbols.functions = symbols.functions.map(x => { - x.position = document.positionAt(x.offset); - return x; + return result; +} + +function findDocumentLinks(document: TextDocument, ast: INode): DocumentLink[] { + const links = ls.findDocumentLinks(document, ast, { + resolveReference: (ref, base = Files.uriToFilePath(document.uri)) => resolveReference(ref, base) }); - return { - symbols, - node: offset ? getNodeAtOffset(ast, offset) : null - }; + return links.map(link => ({ + ...link, + target: link.target.startsWith('file:') ? Files.uriToFilePath(link.target) : link.target + })); } -function getDocumentSymbols(document: TextDocument, settings: ISettings): ISymbols { - try { - return findSymbols(document.getText()); - } catch (err) { - if (settings.showErrors) { - throw err; - } +export function resolveReference(ref: string, base: string): string { + if (ref[0] === '~') { + ref = 'node_modules/' + ref.slice(1); + } + + if (!ref.endsWith('.scss') && !ref.endsWith('.css')) { + ref += '.scss'; + } + + return path.join(path.dirname(base), ref); +} + +function getVariableValue(ast: INode, offset: number): string | null { + const node = getNodeAtOffset(ast, offset); - return { - variables: [], - mixins: [], - functions: [], - imports: [] - }; + if (node === null) { + return null; } + + const parent = getParentNodeByType(node, NodeType.VariableDeclaration); + + return parent?.getValue()?.getText() || null; +} + +function getMethodParameters(ast: INode, offset: number): IVariable[] { + const node = getNodeAtOffset(ast, offset); + + if (node === null) { + return []; + } + + return node + .getParameters() + .getChildren() + .map(child => { + const defaultValueNode = child.getDefaultValue(); + + const value = defaultValueNode === undefined ? null : defaultValueNode.getText(); + + return { + name: child.getName(), + offset: child.offset, + value + }; + }); +} + +export function convertLinksToImports(links: DocumentLink[]): IImport[] { + return links.map(link => ({ + filepath: link.target, + dynamic: reDynamicPath.test(link.target), + css: link.target.endsWith('.css') + })); } diff --git a/src/services/scanner.ts b/src/services/scanner.ts index ecdc1113..80348b44 100644 --- a/src/services/scanner.ts +++ b/src/services/scanner.ts @@ -38,7 +38,7 @@ export default class ScannerService { const content = await this._readFile(filepath); const document = TextDocument.create(originalFilepath, 'scss', 1, content); - const { symbols } = parseDocument(document, null, this._settings); + const { symbols } = parseDocument(document, null); this._storage.set(filepath, { ...symbols, filepath }); diff --git a/src/test/helpers.ts b/src/test/helpers.ts new file mode 100644 index 00000000..ff7cfde6 --- /dev/null +++ b/src/test/helpers.ts @@ -0,0 +1,51 @@ +import { TextDocument, Range, Position } from 'vscode-languageserver'; +import { getSCSSLanguageService } from 'vscode-css-languageservice'; + +import { INode } from '../types/nodes'; +import { ISettings } from '../types/settings'; + +const ls = getSCSSLanguageService(); + +ls.configure({ + validate: false +}); + +export type MakeDocumentOptions = { + uri?: string; + languageId?: string; + version?: number; +}; + +export function makeDocument(lines: string | string[], options: MakeDocumentOptions = {}): TextDocument { + return TextDocument.create( + options.uri || 'index.scss', + options.languageId || 'scss', + options.version || 1, + Array.isArray(lines) ? lines.join('\n') : lines + ); +} + +export function makeAst(lines: string[]): INode { + const document = makeDocument(lines); + + return ls.parseStylesheet(document) as INode; +} + +export function makeSameLineRange(line: number = 1, start: number = 1, end: number = 1): Range { + return Range.create(Position.create(line, start), Position.create(line, end)); +} + +export function makeSettings(options?: Partial): ISettings { + return { + scannerDepth: 30, + scannerExclude: ['**/.git', '**/node_modules', '**/bower_components'], + scanImportedFiles: true, + implicitlyLabel: '(implicitly)', + showErrors: false, + suggestVariables: true, + suggestMixins: true, + suggestFunctions: true, + suggestFunctionsInStringContextAfterSymbols: ' (+-*%', + ...options + }; +} diff --git a/src/test/parser/mixin.spec.ts b/src/test/parser/mixin.spec.ts deleted file mode 100644 index 22a651cb..00000000 --- a/src/test/parser/mixin.spec.ts +++ /dev/null @@ -1,84 +0,0 @@ -'use strict'; - -import * as assert from 'assert'; - -import { TextDocument } from 'vscode-languageserver'; -import { getSCSSLanguageService } from 'vscode-css-languageservice'; - -import { INode, NodeType } from '../../types/nodes'; -import { makeMixin } from '../../parser/mixin'; - -const ls = getSCSSLanguageService(); - -ls.configure({ - lint: false, - validate: false -}); - -function parseText(text: string[], naked = false): INode { - const doc = TextDocument.create('test.scss', 'scss', 1, text.join('\n')); - const ast = ls.parseStylesheet(doc); - - return naked ? ast : ast.getChildren()[0]; -} - -describe('Parser/Mixin', () => { - it('Simple', () => { - const ast = parseText([ - '@mixin a() {', - ' content: "1"', - '}' - ]); - - const mixin = makeMixin(ast); - - assert.equal(mixin.name, 'a'); - assert.equal(mixin.parameters.length, 0); - }); - - it('Parameters', () => { - const ast = parseText([ - '@mixin a($a: 1, $b) {', - ' content: "1";', - '}' - ]); - - const mixin = makeMixin(ast); - - assert.equal(mixin.name, 'a'); - - assert.equal(mixin.parameters[0].name, '$a'); - assert.equal(mixin.parameters[0].value, '1'); - - assert.equal(mixin.parameters[1].name, '$b'); - assert.equal(mixin.parameters[1].value, null); - }); - - it('Nesting', done => { - const ast = parseText([ - '.a-#{$name} {', - ' @mixin b($a: 1, $b) {', - ' content: "1";', - ' }', - '}' - ], true); - - ast.accept((node: INode) => { - if (node.type === NodeType.MixinDeclaration) { - const mixin = makeMixin(node); - - assert.equal(mixin.name, 'b'); - - assert.equal(mixin.parameters[0].name, '$a'); - assert.equal(mixin.parameters[0].value, '1'); - - assert.equal(mixin.parameters[1].name, '$b'); - assert.equal(mixin.parameters[1].value, null); - - done(); - } - - return true; - }); - }); -}); diff --git a/src/test/parser/symbols.spec.ts b/src/test/parser/symbols.spec.ts deleted file mode 100644 index 340f01d6..00000000 --- a/src/test/parser/symbols.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -'use strict'; - -import * as assert from 'assert'; - -import { TextDocument } from 'vscode-languageserver'; -import { getSCSSLanguageService } from 'vscode-css-languageservice'; - -import { INode } from '../../types/nodes'; -import { findSymbols, findSymbolsAtOffset } from '../../parser/symbols'; - -const ls = getSCSSLanguageService(); - -ls.configure({ - lint: false, - validate: false -}); - -function parseText(text: string[]): INode { - const doc = TextDocument.create('test.scss', 'scss', 1, text.join('\n')); - return ls.parseStylesheet(doc); -} - -describe('Parser/Symbols', () => { - it('findSymbols - Variables', () => { - const text = [ - '$a: 1;', - '.a {', - ' $b: 2;', - '}' - ].join('\n'); - - const { variables } = findSymbols(text); - - assert.equal(variables.length, 1); - - assert.equal(variables[0].name, '$a'); - assert.equal(variables[0].value, '1'); - }); - - it('findSymbols - Mixins', () => { - const text = [ - '@mixin a() {}', - '.a {', - ' @mixin b() {}', - '}', - '@mixin c() {', - ' @mixin d() {}', - '}' - ].join('\n'); - - const { mixins } = findSymbols(text); - - assert.equal(mixins.length, 2); - - assert.equal(mixins[0].name, 'a'); - assert.equal(mixins[1].name, 'c'); - }); - - it('findSymbols - Functions', () => { - const text = [ - '@function a($arg) {', - ' @return $arg;', - '}' - ].join('\n'); - - const { functions } = findSymbols(text); - - assert.equal(functions.length, 1); - - assert.equal(functions[0].name, 'a'); - }); - - it('findSymbols - Imports', () => { - const text = [ - '@import "styles.scss";', - '@import "styles.css";', - '@import "@{styles}.scss";', - '@import "styles/**/*.scss";' - ].join('\n'); - - const { imports } = findSymbols(text); - - assert.equal(imports.length, 4); - - assert.equal(imports[0].filepath, 'styles.scss'); - assert.equal(imports[1].filepath, 'styles.css'); - assert.equal(imports[2].filepath, '@{styles}.scss'); - assert.equal(imports[3].filepath, 'styles/**/*.scss'); - }); - - it('findSymbolsAtOffset - Variables', () => { - const ast = parseText([ - '$a: 1;', - '.a {', - ' $b: 2;', - '}' - ]); - - const { variables } = findSymbolsAtOffset(ast, 21); - - assert.equal(variables.length, 1); - - assert.equal(variables[0].name, '$b'); - assert.equal(variables[0].value, '2'); - }); - - it('findSymbolsAtOffset - Mixins', () => { - const ast = parseText([ - '@mixin a() {}', - '.a {', - ' @mixin b() {}', - '}', - '@mixin c() {', - ' @mixin d() {}', - '}' - ]); - - // @mixin a() {__0__} - // .a {__1__ - // @mixin b() {__2__} - // }__3__ - // @mixin c() {__4__ - // @mixin d() {__5__} - // }__6__ - - assert.equal(findSymbolsAtOffset(ast, 12).mixins.length, 0, '__0__'); - assert.equal(findSymbolsAtOffset(ast, 18).mixins.length, 1, '__1__'); - assert.equal(findSymbolsAtOffset(ast, 33).mixins.length, 1, '__2__'); - assert.equal(findSymbolsAtOffset(ast, 36).mixins.length, 1, '__3__'); - assert.equal(findSymbolsAtOffset(ast, 49).mixins.length, 1, '__4__'); - assert.equal(findSymbolsAtOffset(ast, 64).mixins.length, 1, '__5__'); - assert.equal(findSymbolsAtOffset(ast, 67).mixins.length, 1, '__6__'); - }); - - it('findSymbolsAtOffset - Functions', () => { - const ast = parseText([ - '@function name($a: 1) { @return }' - ]); - - const { variables } = findSymbolsAtOffset(ast, 32); - - assert.equal(variables.length, 1); - - assert.equal(variables[0].name, '$a'); - assert.equal(variables[0].value, '1'); - }); -}); diff --git a/src/test/parser/variable.spec.ts b/src/test/parser/variable.spec.ts deleted file mode 100644 index bd5f67ed..00000000 --- a/src/test/parser/variable.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; - -import * as assert from 'assert'; - -import { TextDocument } from 'vscode-languageserver'; -import { getSCSSLanguageService } from 'vscode-css-languageservice'; - -import { INode } from '../../types/nodes'; -import { makeVariable } from '../../parser/variable'; - -const ls = getSCSSLanguageService(); - -ls.configure({ - lint: false, - validate: false -}); - -function parseText(text: string[]): INode { - const doc = TextDocument.create('test.scss', 'scss', 1, text.join('\n')); - return (ls.parseStylesheet(doc)).getChildren()[0]; -} - -describe('Parser/Variable', () => { - it('Simple', () => { - const ast = parseText([ - '$name: 1;' - ]); - - const variable = makeVariable(ast); - - assert.equal(variable.name, '$name'); - assert.equal(variable.value, '1'); - }); - - it('Parameter', () => { - const ast = parseText([ - '@mixin a($a:1, $b) {', - ' content: "1";', - '}' - ]).getParameters().getChildren(); - - const vars = { - one: makeVariable(ast[0], 'a'), - two: makeVariable(ast[1], 'a') - }; - - assert.equal(vars.one.name, '$a'); - assert.equal(vars.one.mixin, 'a'); - assert.equal(vars.one.value, '1'); - - assert.equal(vars.two.name, '$b'); - assert.equal(vars.two.mixin, 'a'); - assert.equal(vars.two.value, null); - }); -}); diff --git a/src/test/providers/completion.spec.ts b/src/test/providers/completion.spec.ts index 470bf7a6..0cc5ecc8 100644 --- a/src/test/providers/completion.spec.ts +++ b/src/test/providers/completion.spec.ts @@ -2,26 +2,12 @@ import * as assert from 'assert'; -import { TextDocument, CompletionItemKind } from 'vscode-languageserver'; +import { CompletionItemKind, CompletionList } from 'vscode-languageserver'; -import { ISettings } from '../../types/settings'; import StorageService from '../../services/storage'; import { doCompletion } from '../../providers/completion'; - -const settings = { - scannerExclude: [], - scannerDepth: 20, - showErrors: false, - implicitlyLabel: '(implicitly)', - suggestMixins: true, - suggestVariables: true, - suggestFunctions: true, - suggestFunctionsInStringContextAfterSymbols: ' (+-*%' -}; - -function makeDocument(lines: string | string[]) { - return TextDocument.create('test.scss', 'scss', 1, Array.isArray(lines) ? lines.join('\n') : lines); -} +import * as helpers from '../helpers'; +import { ISettings } from '../../types/settings'; const storage = new StorageService(); @@ -44,87 +30,110 @@ storage.set('one.scss', { imports: [] }); +function getCompletionList(lines: string[], options?: Partial): CompletionList { + const text = lines.join('\n'); + + const settings = helpers.makeSettings(options); + const document = helpers.makeDocument(text); + const offset = text.indexOf('|'); + + return doCompletion(document, offset, settings, storage); +} + describe('Providers/Completion - Basic', () => { it('Variables', () => { - const doc = makeDocument('$'); - assert.equal(doCompletion(doc, 1, settings, storage).items.length, 5); + const actual = getCompletionList(['$|']); + + assert.equal(actual.items.length, 5); }); it('Mixins', () => { - const doc = makeDocument('@include '); - assert.equal(doCompletion(doc, 9, settings, storage).items.length, 1); + const actual = getCompletionList(['@include |']); + + assert.equal(actual.items.length, 1); }); }); describe('Providers/Completion - Context', () => { it('Empty property value', () => { - const doc = makeDocument('.a { content: }'); - assert.equal(doCompletion(doc, 14, settings, storage).items.length, 5); + const actual = getCompletionList(['.a { content: | }']); + + assert.equal(actual.items.length, 5); }); it('Non-empty property value without suggestions', () => { - const doc = makeDocument('.a { background: url(../images/one.png); }'); - assert.equal(doCompletion(doc, 34, settings, storage).items.length, 0); + const actual = getCompletionList(['.a { background: url(../images/one|.png); }']); + + assert.equal(actual.items.length, 0); }); it('Non-empty property value with Variables', () => { - const doc = makeDocument('.a { background: url(../images/#{$one}/one.png); }'); - assert.equal(doCompletion(doc, 37, settings, storage).items.length, 5, 'True'); - assert.equal(doCompletion(doc, 42, settings, storage).items.length, 0, 'False'); + const actual = getCompletionList(['.a { background: url(../images/#{$one|}/one.png); }']); + + assert.equal(actual.items.length, 5); }); it('Discard suggestions inside quotes', () => { - const doc = makeDocument('.a { background: url("../images/#{$one}/$one.png"); @include test("test", $one); }'); - assert.equal(doCompletion(doc, 44, settings, storage).items.length, 0, 'Hide'); - assert.equal(doCompletion(doc, 38, settings, storage).items.length, 6, 'True'); - assert.equal(doCompletion(doc, 78, settings, storage).items.length, 5, 'Mixin'); + const actual = getCompletionList([ + '.a {', + ' background: url("../images/#{$one}/$one|.png");', + '}' + ]); + + assert.equal(actual.items.length, 0); }); it('Custom value for `suggestFunctionsInStringContextAfterSymbols` option', () => { - const doc = makeDocument('.a { background: url(../images/m'); - const options = Object.assign(settings, { + const actual = getCompletionList(['.a { background: url(../images/m|'], { suggestFunctionsInStringContextAfterSymbols: '/' }); - assert.equal(doCompletion(doc, 32, options, storage).items.length, 1); + + assert.equal(actual.items.length, 1); }); it('Discard suggestions inside single-line comments', () => { - const doc = makeDocument('// $'); - assert.equal(doCompletion(doc, 4, settings, storage).items.length, 0); + const actual = getCompletionList(['// $|']); + + assert.equal(actual.items.length, 0); }); it('Discard suggestions inside block comments', () => { - const doc = makeDocument('/* $ */'); - assert.equal(doCompletion(doc, 4, settings, storage).items.length, 0); + const actual = getCompletionList(['/* $| */']); + + assert.equal(actual.items.length, 0); }); it('Identify color variables', () => { - const doc = makeDocument('$'); - const completion = doCompletion(doc, 1, settings, storage); - - assert.equal(completion.items[0].kind, CompletionItemKind.Variable); - assert.equal(completion.items[1].kind, CompletionItemKind.Variable); - assert.equal(completion.items[2].kind, CompletionItemKind.Color); - assert.equal(completion.items[3].kind, CompletionItemKind.Color); - assert.equal(completion.items[4].kind, CompletionItemKind.Color); + const actual = getCompletionList(['$|']); + + assert.equal(actual.items[0].kind, CompletionItemKind.Variable); + assert.equal(actual.items[1].kind, CompletionItemKind.Variable); + assert.equal(actual.items[2].kind, CompletionItemKind.Color); + assert.equal(actual.items[3].kind, CompletionItemKind.Color); + assert.equal(actual.items[4].kind, CompletionItemKind.Color); }); }); describe('Providers/Completion - Implicitly', () => { it('Show default implicitly label', () => { - const doc = makeDocument('$'); - assert.equal(doCompletion(doc, 1, settings, storage).items[0].detail, '(implicitly) one.scss'); + const actual = getCompletionList(['$|']); + + assert.equal(actual.items[0].detail, '(implicitly) one.scss'); }); it('Show custom implicitly label', () => { - const doc = makeDocument('$'); - settings.implicitlyLabel = '👻'; - assert.equal(doCompletion(doc, 1, settings, storage).items[0].detail, '👻 one.scss'); + const actual = getCompletionList(['$|'], { + implicitlyLabel: '👻' + }); + + assert.equal(actual.items[0].detail, '👻 one.scss'); }); it('Hide implicitly label', () => { - const doc = makeDocument('$'); - settings.implicitlyLabel = null; - assert.equal(doCompletion(doc, 1, settings, storage).items[0].detail, 'one.scss'); + const actual = getCompletionList(['$|'], { + implicitlyLabel: null + }); + + assert.equal(actual.items[0].detail, 'one.scss'); }); }); diff --git a/src/test/providers/goDefinition.spec.ts b/src/test/providers/goDefinition.spec.ts index 7864eef1..80a4f1ff 100644 --- a/src/test/providers/goDefinition.spec.ts +++ b/src/test/providers/goDefinition.spec.ts @@ -2,23 +2,11 @@ import * as assert from 'assert'; -import { TextDocument, Files } from 'vscode-languageserver'; +import { Files } from 'vscode-languageserver'; -import { ISettings } from '../../types/settings'; import StorageService from '../../services/storage'; import { goDefinition } from '../../providers/goDefinition'; - -const settings = { - scannerExclude: [], - scannerDepth: 20, - showErrors: false, - suggestMixins: true, - suggestVariables: true -}; - -function makeDocument(lines: string | string[]) { - return TextDocument.create('test.scss', 'scss', 1, Array.isArray(lines) ? lines.join('\n') : lines); -} +import * as helpers from '../helpers'; const storage = new StorageService(); @@ -38,79 +26,79 @@ storage.set('one.scss', { }); describe('Providers/GoDefinition', () => { - it('doGoDefinition - Variables', () => { - const doc = makeDocument('.a { content: $a; }'); - - return goDefinition(doc, 15, storage, settings).then(result => { - assert.ok(Files.uriToFilePath(result.uri), 'one.scss'); - assert.deepEqual(result.range, { - start: { line: 1, character: 1 }, - end: { line: 1, character: 3 } - }); + it('doGoDefinition - Variables', async () => { + const document = helpers.makeDocument('.a { content: $a; }'); + + const actual = await goDefinition(document, 15, storage); + + assert.ok(Files.uriToFilePath(actual.uri), 'one.scss'); + assert.deepEqual(actual.range, { + start: { line: 1, character: 1 }, + end: { line: 1, character: 3 } }); }); - it('doGoDefinition - Variable definition', () => { - const doc = makeDocument('$a: 1;'); + it('doGoDefinition - Variable definition', async () => { + const document = helpers.makeDocument('$a: 1;'); - return goDefinition(doc, 2, storage, settings).then(result => { - assert.equal(result, null); - }); + const actual = await goDefinition(document, 2, storage); + + assert.equal(actual, null); }); - it('doGoDefinition - Mixins', () => { - const doc = makeDocument('.a { @include mixin(); }'); + it('doGoDefinition - Mixins', async () => { + const document = helpers.makeDocument('.a { @include mixin(); }'); - return goDefinition(doc, 16, storage, settings).then(result => { - assert.ok(Files.uriToFilePath(result.uri), 'one.scss'); - assert.deepEqual(result.range, { - start: { line: 1, character: 1 }, - end: { line: 1, character: 6 } - }); + const actual = await goDefinition(document, 16, storage); + + assert.ok(Files.uriToFilePath(actual.uri), 'one.scss'); + assert.deepEqual(actual.range, { + start: { line: 1, character: 1 }, + end: { line: 1, character: 6 } }); }); - it('doGoDefinition - Mixin definition', () => { - const doc = makeDocument('@mixin mixin($a) {}'); + it('doGoDefinition - Mixin definition', async () => { + const document = helpers.makeDocument('@mixin mixin($a) {}'); - return goDefinition(doc, 8, storage, settings).then(result => { - assert.equal(result, null); - }); + const actual = await goDefinition(document, 8, storage); + + assert.equal(actual, null); }); - it('doGoDefinition - Mixin Arguments', () => { - const doc = makeDocument('@mixin mixin($a) {}'); + it('doGoDefinition - Mixin Arguments', async () => { + const document = helpers.makeDocument('@mixin mixin($a) {}'); - return goDefinition(doc, 10, storage, settings).then(result => { - assert.equal(result, null); - }); + const actual = await goDefinition(document, 10, storage); + + assert.equal(actual, null); }); - it('doGoDefinition - Functions', () => { - const doc = makeDocument('.a { content: make(1); }'); + it('doGoDefinition - Functions', async () => { + const document = helpers.makeDocument('.a { content: make(1); }'); + + const actual = await goDefinition(document, 16, storage); - return goDefinition(doc, 16, storage, settings).then(result => { - assert.ok(Files.uriToFilePath(result.uri), 'one.scss'); - assert.deepEqual(result.range, { - start: { line: 1, character: 1 }, - end: { line: 1, character: 5 } - }); + assert.ok(Files.uriToFilePath(actual.uri), 'one.scss'); + assert.deepEqual(actual.range, { + start: { line: 1, character: 1 }, + end: { line: 1, character: 5 } }); }); - it('doGoDefinition - Function definition', () => { - const doc = makeDocument('@function make($a) {}'); + it('doGoDefinition - Function definition', async () => { + const document = helpers.makeDocument('@function make($a) {}'); - return goDefinition(doc, 8, storage, settings).then(result => { - assert.equal(result, null); - }); + const actual = await goDefinition(document, 8, storage); + + assert.equal(actual, null); }); - it('doGoDefinition - Function Arguments', () => { - const doc = makeDocument('@function make($a) {}'); + it('doGoDefinition - Function Arguments', async () => { + const document = helpers.makeDocument('@function make($a) {}'); - return goDefinition(doc, 13, storage, settings).then(result => { - assert.equal(result, null); - }); + const actual = await goDefinition(document, 13, storage); + + assert.equal(actual, null); }); }); diff --git a/src/test/providers/hover.spec.ts b/src/test/providers/hover.spec.ts index c13cd2ca..e766fc5f 100644 --- a/src/test/providers/hover.spec.ts +++ b/src/test/providers/hover.spec.ts @@ -2,82 +2,82 @@ import * as assert from 'assert'; -import { TextDocument } from 'vscode-languageserver'; +import { Hover } from 'vscode-languageserver'; import StorageService from '../../services/storage'; import { doHover } from '../../providers/hover'; -import { ISettings } from '../../types/settings'; +import * as helpers from '../helpers'; const storage = new StorageService(); -const settings = { - scannerExclude: [], - scannerDepth: 20, - showErrors: false, - suggestMixins: true, - suggestVariables: true, - suggestFunctions: true -}; - -interface IHover { - language: string; - value: string; -} +storage.set('file.scss', { + document: 'file.scss', + filepath: 'file.scss', + variables: [ + { name: '$variable', value: null, offset: 0, position: { line: 1, character: 1 } } + ], + mixins: [ + { name: 'mixin', parameters: [], offset: 0, position: { line: 1, character: 1 } } + ], + functions: [ + { name: 'make', parameters: [], offset: 0, position: { line: 1, character: 1 } } + ], + imports: [] +}); + +function getHover(lines: string[]): Hover | null { + const text = lines.join('\n'); + + const document = helpers.makeDocument(text); + const offset = text.indexOf('|'); -function makeDocument(lines: string | string[]) { - return TextDocument.create('test.scss', 'scss', 1, Array.isArray(lines) ? lines.join('\n') : lines); + return doHover(document, offset, storage); } describe('Providers/Hover', () => { - it('doHover - Variables', () => { - const doc = makeDocument([ + it('should suggest local symbols', () => { + const actual = getHover([ '$one: 1;', - '.a { content: $one; }' + '.a { content: $one|; }' + ]); + + assert.deepStrictEqual(actual.contents, { + language: 'scss', + value: '$one: 1;' + }); + }); + + it('should suggest global variables', () => { + const actual = getHover([ + '.a { content: $variable|; }' ]); - // $o| - assert.equal(doHover(doc, 2, storage, settings), null); - // .a { content: $o| - assert.equal((doHover(doc, 25, storage, settings).contents).value, '$one: 1;'); + assert.deepStrictEqual(actual.contents, { + language: 'scss', + value: '$variable: null;\n@import "file.scss" (implicitly)' + }); }); - it('doHover - Mixins', () => { - const doc = makeDocument([ - '@mixin one($a) { content: "nope"; }', - '@include one(1);' + it('should suggest global mixins', () => { + const actual = getHover([ + '@include mixin|' ]); - // @m| - assert.equal(doHover(doc, 2, storage, settings), null); - // @mixin on| - assert.equal(doHover(doc, 9, storage, settings), null); - // @mixin one($| - assert.equal(doHover(doc, 12, storage, settings), null); - // @mixin one($a) { con| - assert.equal(doHover(doc, 20, storage, settings), null); - // @mixin one($a) { content: "no| - assert.equal(doHover(doc, 29, storage, settings), null); - // @inc| - assert.equal((doHover(doc, 40, storage, settings).contents).value, '@mixin one($a: null) {…}'); - // @include on| - assert.equal((doHover(doc, 47, storage, settings).contents).value, '@mixin one($a: null) {…}'); + assert.deepStrictEqual(actual.contents, { + language: 'scss', + value: '@mixin mixin() {…}\n@import "file.scss" (implicitly)' + }); }); - it('doHover - Functions', () => { - const doc = makeDocument([ - '@function make($a) { @return $a; }', - '.hi { content: make(1); }' + // Does not work right now + it.skip('should suggest global functions', () => { + const actual = getHover([ + '.a { content: make|(); }' ]); - // @f| - assert.equal(doHover(doc, 2, storage, settings), null); - // @function ma| - assert.equal(doHover(doc, 12, storage, settings), null); - // @function make($a) { @re| - assert.equal(doHover(doc, 24, storage, settings), null); - // @function make($a) { @return $| - assert.equal((doHover(doc, 30, storage, settings).contents).value, '$a: null;'); - // .hi { content: ma| - assert.equal((doHover(doc, 52, storage, settings).contents).value, '@function make($a: null) {…}'); + assert.deepStrictEqual(actual.contents, { + language: 'scss', + value: '@function make($a: null) {…}\n@import "file.scss" (implicitly)' + }); }); }); diff --git a/src/test/providers/signatureHelp.spec.ts b/src/test/providers/signatureHelp.spec.ts index 1a190d9e..771847e3 100644 --- a/src/test/providers/signatureHelp.spec.ts +++ b/src/test/providers/signatureHelp.spec.ts @@ -2,23 +2,11 @@ import * as assert from 'assert'; -import { TextDocument } from 'vscode-languageserver'; +import { SignatureHelp } from 'vscode-languageserver'; -import { ISettings } from '../../types/settings'; import StorageService from '../../services/storage'; import { doSignatureHelp } from '../../providers/signatureHelp'; - -const settings = { - scannerExclude: [], - scannerDepth: 20, - showErrors: false, - suggestMixins: true, - suggestVariables: true -}; - -function makeDocument(lines: string | string[]) { - return TextDocument.create('test.scss', 'scss', 1, Array.isArray(lines) ? lines.join('\n') : lines); -} +import * as helpers from '../helpers'; const storage = new StorageService(); @@ -72,170 +60,174 @@ storage.set('one.scss', { imports: [] }); +function getSignatureHelp(lines: string[]): SignatureHelp { + const text = lines.join('\n'); + + const document = helpers.makeDocument(text); + const offset = text.indexOf('|'); + + return doSignatureHelp(document, offset, storage); +} + describe('Providers/SignatureHelp - Empty', () => { it('Empty', () => { - const doc = makeDocument('@include one('); - assert.equal(doSignatureHelp(doc, 13, storage, settings).signatures.length, 1); + const actual = getSignatureHelp(['@include one(|']); + + assert.equal(actual.signatures.length, 1); }); it('Closed without parameters', () => { - const doc = makeDocument('@include two()'); - assert.equal(doSignatureHelp(doc, 13, storage, settings).signatures.length, 3); + const actual = getSignatureHelp(['@include two(|)']); + + assert.equal(actual.signatures.length, 3); }); it('Closed with parameters', () => { - const doc = makeDocument('@include two(1);'); - assert.equal(doSignatureHelp(doc, 16, storage, settings).signatures.length, 0); + const actual = getSignatureHelp(['@include two(1);']); + + assert.equal(actual.signatures.length, 0); }); }); describe('Providers/SignatureHelp - Two parameters', () => { it('Passed one parameter of two', () => { - const doc = makeDocument('@include two(1,'); - const signature = doSignatureHelp(doc, 15, storage, settings); + const actual = getSignatureHelp(['@include two(1,|']); - assert.equal(signature.activeParameter, 1, 'activeParameter'); - assert.equal(signature.signatures.length, 2, 'signatures.length'); + assert.equal(actual.activeParameter, 1, 'activeParameter'); + assert.equal(actual.signatures.length, 2, 'signatures.length'); }); it('Passed two parameter of two', () => { - const doc = makeDocument('@include two(1, 2,'); - const signature = doSignatureHelp(doc, 18, storage, settings); + const actual = getSignatureHelp(['@include two(1, 2,|']); - assert.equal(signature.activeParameter, 2, 'activeParameter'); - assert.equal(signature.signatures.length, 1, 'signatures.length'); + assert.equal(actual.activeParameter, 2, 'activeParameter'); + assert.equal(actual.signatures.length, 1, 'signatures.length'); }); it('Passed three parameters of two', () => { - const doc = makeDocument('@include two(1, 2, 3,'); - const signature = doSignatureHelp(doc, 21, storage, settings); + const actual = getSignatureHelp(['@include two(1, 2, 3,|']); - assert.equal(signature.signatures.length, 0); + assert.equal(actual.signatures.length, 0); }); it('Passed two parameter of two with parenthesis', () => { - const doc = makeDocument('@include two(1, 2)'); - const signature = doSignatureHelp(doc, 18, storage, settings); + const actual = getSignatureHelp(['@include two(1, 2)|']); - assert.equal(signature.signatures.length, 0); + assert.equal(actual.signatures.length, 0); }); }); describe('Providers/SignatureHelp - parseArgumentsAtLine for Mixins', () => { it('RGBA', () => { - const doc = makeDocument('@include two(rgba(0,0,0,.0001),'); - const signature = doSignatureHelp(doc, 31, storage, settings); + const actual = getSignatureHelp(['@include two(rgba(0,0,0,.0001),|']); - assert.equal(signature.activeParameter, 1, 'activeParameter'); - assert.equal(signature.signatures.length, 2, 'signatures.length'); + assert.equal(actual.activeParameter, 1, 'activeParameter'); + assert.equal(actual.signatures.length, 2, 'signatures.length'); }); it('RGBA when typing', () => { - const doc = makeDocument('@include two(rgba(0,0,0,'); - const signature = doSignatureHelp(doc, 24, storage, settings); + const actual = getSignatureHelp(['@include two(rgba(0,0,0,|']); - assert.equal(signature.activeParameter, 0, 'activeParameter'); - assert.equal(signature.signatures.length, 3, 'signatures.length'); + assert.equal(actual.activeParameter, 0, 'activeParameter'); + assert.equal(actual.signatures.length, 3, 'signatures.length'); }); it('Quotes', () => { - const doc = makeDocument('@include two("\\",;",'); - const signature = doSignatureHelp(doc, 20, storage, settings); + const actual = getSignatureHelp(['@include two("\\",;",|']); - assert.equal(signature.activeParameter, 1, 'activeParameter'); - assert.equal(signature.signatures.length, 2, 'signatures.length'); + assert.equal(actual.activeParameter, 1, 'activeParameter'); + assert.equal(actual.signatures.length, 2, 'signatures.length'); }); it('With overload', () => { - const doc = makeDocument('@include two('); - assert.equal(doSignatureHelp(doc, 13, storage, settings).signatures.length, 3); + const actual = getSignatureHelp(['@include two(|']); + + assert.equal(actual.signatures.length, 3); }); it('Single-line selector', () => { - const doc = makeDocument('h1 { @include two(1, }'); - assert.equal(doSignatureHelp(doc, 20, storage, settings).signatures.length, 2); + const actual = getSignatureHelp(['h1 { @include two(1,| }']); + + assert.equal(actual.signatures.length, 2); }); it('Single-line Mixin reference', () => { - const doc = makeDocument('h1 { @include two(1, 2); @include two(1,) }'); - assert.equal(doSignatureHelp(doc, 40, storage, settings).signatures.length, 2); + const actual = getSignatureHelp([ + 'h1 {', + ' @include two(1, 2);', + ' @include two(1,|)', + '}']); + + assert.equal(actual.signatures.length, 2); }); it('Mixin with named argument', () => { - const doc = makeDocument('@include two($a: 1,'); - assert.equal(doSignatureHelp(doc, 19, storage, settings).signatures.length, 2); + const actual = getSignatureHelp(['@include two($a: 1,|']); + + assert.equal(actual.signatures.length, 2); }); }); describe('Providers/SignatureHelp - parseArgumentsAtLine for Functions', () => { it('Empty', () => { - const doc = makeDocument('content: make('); - const signatures = doSignatureHelp(doc, 14, storage, settings).signatures; + const actual = getSignatureHelp(['content: make(|']); - assert.equal(signatures.length, 1, 'length'); - assert.ok(signatures[0].label.startsWith('make'), 'name'); + assert.equal(actual.signatures.length, 1, 'length'); + assert.ok(actual.signatures[0].label.startsWith('make'), 'name'); }); it('Single-line Function reference', () => { - const doc = makeDocument('content: make()+make('); - const signatures = doSignatureHelp(doc, 21, storage, settings).signatures; + const actual = getSignatureHelp(['content: make()+make(|']); - assert.equal(signatures.length, 1, 'length'); - assert.ok(signatures[0].label.startsWith('make'), 'name'); + assert.equal(actual.signatures.length, 1, 'length'); + assert.ok(actual.signatures[0].label.startsWith('make'), 'name'); }); it('Inside another uncompleted function', () => { - const doc = makeDocument('content: attr(make('); - const signatures = doSignatureHelp(doc, 19, storage, settings).signatures; + const actual = getSignatureHelp(['content: attr(make(|']); - assert.equal(signatures.length, 1, 'length'); - assert.ok(signatures[0].label.startsWith('make'), 'name'); + assert.equal(actual.signatures.length, 1, 'length'); + assert.ok(actual.signatures[0].label.startsWith('make'), 'name'); }); it('Inside another completed function', () => { - const doc = makeDocument('content: attr(one(1, two(1, two(1, 2)),'); - const signatures = doSignatureHelp(doc, 39, storage, settings).signatures; + const actual = getSignatureHelp(['content: attr(one(1, two(1, two(1, 2)),|']); - assert.equal(signatures.length, 1, 'length'); - assert.ok(signatures[0].label.startsWith('one'), 'name'); + assert.equal(actual.signatures.length, 1, 'length'); + assert.ok(actual.signatures[0].label.startsWith('one'), 'name'); }); it('Inside several completed functions', () => { - const doc = makeDocument('background: url(one(1, one(1, 2, two(1, 2)),'); - const signatures = doSignatureHelp(doc, 44, storage, settings).signatures; + const actual = getSignatureHelp(['background: url(one(1, one(1, 2, two(1, 2)),|']); - assert.equal(signatures.length, 1, 'length'); - assert.ok(signatures[0].label.startsWith('one'), 'name'); + assert.equal(actual.signatures.length, 1, 'length'); + assert.ok(actual.signatures[0].label.startsWith('one'), 'name'); }); it('Inside another function with CSS function', () => { - const doc = makeDocument('background-color: make(rgba('); - const signatures = doSignatureHelp(doc, 28, storage, settings).signatures; + const actual = getSignatureHelp(['background-color: make(rgba(|']); - assert.equal(signatures.length, 1, 'length'); - assert.ok(signatures[0].label.startsWith('make'), 'name'); + assert.equal(actual.signatures.length, 1, 'length'); + assert.ok(actual.signatures[0].label.startsWith('make'), 'name'); }); it('Inside another function with uncompleted CSS function', () => { - const doc = makeDocument('background-color: make(rgba(1, 1,2,'); - const signatures = doSignatureHelp(doc, 35, storage, settings).signatures; + const actual = getSignatureHelp(['background-color: make(rgba(1, 1,2,|']); - assert.equal(signatures.length, 1, 'length'); - assert.ok(signatures[0].label.startsWith('make'), 'name'); + assert.equal(actual.signatures.length, 1, 'length'); + assert.ok(actual.signatures[0].label.startsWith('make'), 'name'); }); it('Inside another function with completed CSS function', () => { - const doc = makeDocument('background-color: make(rgba(1,2, 3,.5)'); - const signatures = doSignatureHelp(doc, 38, storage, settings).signatures; + const actual = getSignatureHelp(['background-color: make(rgba(1,2, 3,.5)|']); - assert.equal(signatures.length, 1, 'length'); - assert.ok(signatures[0].label.startsWith('make'), 'name'); + assert.equal(actual.signatures.length, 1, 'length'); + assert.ok(actual.signatures[0].label.startsWith('make'), 'name'); }); it('Interpolation', () => { - const doc = makeDocument('background-color: "#{make(}"'); - const signatures = doSignatureHelp(doc, 26, storage, settings).signatures; + const actual = getSignatureHelp(['background-color: "#{make(|}"']); - assert.equal(signatures.length, 1, 'length'); - assert.ok(signatures[0].label.startsWith('make'), 'name'); + assert.equal(actual.signatures.length, 1, 'length'); + assert.ok(actual.signatures[0].label.startsWith('make'), 'name'); }); }); diff --git a/src/test/services/parser.spec.ts b/src/test/services/parser.spec.ts index bae19b64..9b1d3cdf 100644 --- a/src/test/services/parser.spec.ts +++ b/src/test/services/parser.spec.ts @@ -1,96 +1,150 @@ 'use strict'; import * as assert from 'assert'; +import * as path from 'path'; -import { TextDocument } from 'vscode-languageserver'; +import { parseDocument, convertLinksToImports, resolveReference } from '../../services/parser'; +import * as helpers from '../helpers'; +import { NodeType } from '../../types/nodes'; +import { DocumentLink } from 'vscode-languageclient'; +import { IImport } from '../../types/symbols'; -import { parseDocument } from '../../services/parser'; -import { ISettings } from '../../types/settings'; +describe('Services/Parser', () => { + describe('.parseDocument', () => { + it('should return symbols', () => { + const document = helpers.makeDocument([ + '@import "file.scss";', + '$name: "value";', + '@mixin mixin($a: 1, $b) {}', + '@function function($a: 1, $b) {}' + ]); -function parseText(text: string[]): TextDocument { - return TextDocument.create('test.scss', 'scss', 1, text.join('\n')); -} + const { symbols } = parseDocument(document, null); -describe('Services/Parser', () => { - it('Find symbols without offset position', () => { - const doc = parseText([ - '$name: "value";', - '@mixin mixin($a: 1, $b) {}' - ]); - - const { symbols } = parseDocument(doc, null, { - showErrors: false + // Variables + assert.equal(symbols.variables.length, 1); + + assert.equal(symbols.variables[0].name, '$name'); + assert.equal(symbols.variables[0].value, '"value"'); + + // Mixins + assert.equal(symbols.mixins.length, 1); + + assert.equal(symbols.mixins[0].name, 'mixin'); + assert.equal(symbols.mixins[0].parameters.length, 2); + + assert.equal(symbols.mixins[0].parameters[0].name, '$a'); + assert.equal(symbols.mixins[0].parameters[0].value, '1'); + + assert.equal(symbols.mixins[0].parameters[1].name, '$b'); + assert.equal(symbols.mixins[0].parameters[1].value, null); + + // Functions + assert.equal(symbols.functions.length, 1); + + assert.equal(symbols.functions[0].name, 'function'); + assert.equal(symbols.functions[0].parameters.length, 2); + + assert.equal(symbols.functions[0].parameters[0].name, '$a'); + assert.equal(symbols.functions[0].parameters[0].value, '1'); + + assert.equal(symbols.functions[0].parameters[1].name, '$b'); + assert.equal(symbols.functions[0].parameters[1].value, null); + + // Imports + assert.equal(symbols.imports.length, 1); + + assert.equal(symbols.imports[0].filepath, 'file.scss'); }); - // Variables - assert.equal(symbols.variables.length, 1); + it('should include references as imports', () => { + const document = helpers.makeDocument([ + '// ' + ]); - assert.equal(symbols.variables[0].name, '$name'); - assert.equal(symbols.variables[0].value, '"value"'); + const { symbols } = parseDocument(document); - // Mixins - assert.equal(symbols.mixins.length, 1); + assert.equal(symbols.imports[0].filepath, 'file.scss'); + assert.ok(symbols.imports[0].reference); + }); - assert.equal(symbols.mixins[0].name, 'mixin'); - assert.equal(symbols.mixins[0].parameters.length, 2); + it('should return Node at offset', () => { + const lines = [ + '$name: "value";', + '@mixin mixin($a: 1, $b) {}', + '.test {', + ' content: a|;', + '}' + ]; - assert.equal(symbols.mixins[0].parameters[0].name, '$a'); - assert.equal(symbols.mixins[0].parameters[0].value, '1'); + const document = helpers.makeDocument(lines); + const offset = lines.join('\n').indexOf('|'); - assert.equal(symbols.mixins[0].parameters[1].name, '$b'); - assert.equal(symbols.mixins[0].parameters[1].value, null); + const { node } = parseDocument(document, offset); - // Imports - assert.equal(symbols.imports.length, 0); + assert.equal(node.type, NodeType.Identifier); + }); }); - it('Find symbols with offset position', () => { - const doc = parseText([ - '$name: "value";', - '@function func($a) { @return $a }', - '@mixin mixin($a: 1, $b) {', - ' content: ', - '}' - ]); - - const { symbols } = parseDocument(doc, 87, { - showErrors: false + describe('.resolveReference', () => { + it('should return reference to the node_modules directory', () => { + const expected = path.join('.', 'node_modules', 'foo.scss'); + + const actual = resolveReference('~foo.scss', '.'); + + assert.strictEqual(actual, expected); + }); + + it('should add default extension', () => { + const expected = '_foo.scss'; + + const actual = resolveReference('_foo', '.'); + + assert.strictEqual(actual, expected); }); + }); - // Variables - assert.equal(symbols.variables.length, 3); + describe('.convertLinksToImports', () => { + it('should convert links to imports', () => { + const links: DocumentLink[] = [ + { target: '_partial.scss', range: helpers.makeSameLineRange() } + ]; - assert.equal(symbols.variables[0].name, '$name'); - assert.equal(symbols.variables[0].value, '"value"'); + const expected: IImport[] = [ + { filepath: '_partial.scss', dynamic: false, css: false } + ]; - assert.equal(symbols.variables[1].name, '$a'); - assert.equal(symbols.variables[1].value, '1'); + const actual = convertLinksToImports(links); - assert.equal(symbols.variables[2].name, '$b'); - assert.equal(symbols.variables[2].value, null); + assert.deepStrictEqual(actual, expected); + }); - // Mixins - assert.equal(symbols.mixins.length, 1); + it('should convert dynamic links to imports', () => { + const links: DocumentLink[] = [ + { target: '**/*.scss', range: helpers.makeSameLineRange() } + ]; - assert.equal(symbols.mixins[0].name, 'mixin'); - assert.equal(symbols.mixins[0].parameters.length, 2); + const expected: IImport[] = [ + { filepath: '**/*.scss', dynamic: true, css: false } + ]; - assert.equal(symbols.mixins[0].parameters[0].name, '$a'); - assert.equal(symbols.mixins[0].parameters[0].value, '1'); + const actual = convertLinksToImports(links); - assert.equal(symbols.mixins[0].parameters[1].name, '$b'); - assert.equal(symbols.mixins[0].parameters[1].value, null); + assert.deepStrictEqual(actual, expected); + }); - // Functions - assert.equal(symbols.functions.length, 1); + it('should convert css links to imports', () => { + const links: DocumentLink[] = [ + { target: 'file.css', range: helpers.makeSameLineRange() } + ]; - assert.equal(symbols.functions[0].name, 'func'); - assert.equal(symbols.functions[0].parameters.length, 1); + const expected: IImport[] = [ + { filepath: 'file.css', dynamic: false, css: true } + ]; - assert.equal(symbols.functions[0].parameters[0].name, '$a'); - assert.equal(symbols.functions[0].parameters[0].value, null); + const actual = convertLinksToImports(links); - // Imports - assert.equal(symbols.imports.length, 0); + assert.deepStrictEqual(actual, expected); + }); }); }); diff --git a/src/test/services/scanner.spec.ts b/src/test/services/scanner.spec.ts index dc0cb18a..25e296ae 100644 --- a/src/test/services/scanner.spec.ts +++ b/src/test/services/scanner.spec.ts @@ -5,8 +5,8 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import StorageService from '../../services/storage'; -import { ISettings } from '../../types/settings'; import ScannerService from '../../services/scanner'; +import * as helpers from '../helpers'; class ScannerServiceTest extends ScannerService { protected _readFile = sinon.stub(); @@ -25,7 +25,7 @@ describe('Services/Scanner', () => { describe('.scan', () => { it('should find files and update cache', async () => { const storage = new StorageService(); - const settings = {} as ISettings; + const settings = helpers.makeSettings(); const scanner = new ScannerServiceTest(storage, settings); scanner.fileExistsStub.resolves(true); @@ -44,9 +44,7 @@ describe('Services/Scanner', () => { it('should find file and imported files', async () => { const storage = new StorageService(); - const settings = { - scanImportedFiles: true - } as ISettings; + const settings = helpers.makeSettings(); const scanner = new ScannerServiceTest(storage, settings); scanner.fileExistsStub.resolves(true); @@ -63,9 +61,7 @@ describe('Services/Scanner', () => { it('should do not find imported files when it not required', async () => { const storage = new StorageService(); - const settings = { - scanImportedFiles: false - } as ISettings; + const settings = helpers.makeSettings({ scanImportedFiles: false }); const scanner = new ScannerServiceTest(storage, settings); scanner.fileExistsStub.resolves(true); @@ -82,9 +78,7 @@ describe('Services/Scanner', () => { it('should do not find imported files when the recursive mode is no required', async () => { const storage = new StorageService(); - const settings = { - scanImportedFiles: true - } as ISettings; + const settings = helpers.makeSettings(); const scanner = new ScannerServiceTest(storage, settings); scanner.fileExistsStub.resolves(true); diff --git a/src/test/utils/ast.spec.ts b/src/test/utils/ast.spec.ts index 6b6a1eaf..92677607 100644 --- a/src/test/utils/ast.spec.ts +++ b/src/test/utils/ast.spec.ts @@ -2,32 +2,16 @@ import * as assert from 'assert'; -import { TextDocument } from 'vscode-languageserver'; -import { getSCSSLanguageService } from 'vscode-css-languageservice'; - -import { INode, NodeType } from '../../types/nodes'; +import { NodeType } from '../../types/nodes'; import { getNodeAtOffset, - getParentNodeByType, - hasParentsByType, - getChildByType + getParentNodeByType } from '../../utils/ast'; - -const ls = getSCSSLanguageService(); - -ls.configure({ - lint: false, - validate: false -}); - -function parseText(text: string[]): INode { - const doc = TextDocument.create('test.dcdd', 'dcdd', 1, text.join('\n')); - return ls.parseStylesheet(doc); -} +import * as helpers from '../helpers'; describe('Utils/Ast', () => { it('getNodeAtOffset', () => { - const ast = parseText([ + const ast = helpers.makeAst([ '.a {}' ]); @@ -38,7 +22,7 @@ describe('Utils/Ast', () => { }); it('getParentNodeByType', () => { - const ast = parseText([ + const ast = helpers.makeAst([ '.a {}' ]); @@ -48,27 +32,4 @@ describe('Utils/Ast', () => { assert.equal(parentNode.type, NodeType.Ruleset); assert.equal(parentNode.getText(), '.a {}'); }); - - it('hasParentsByType', () => { - const ast = parseText([ - '@mixin a() {', - ' $name: 1;', - '}' - ]); - - // Stylesheet -> MixinDeclaration -> Nodelist - const node = ast.getChild(0).getChild(1); - - assert.ok(hasParentsByType(node, [NodeType.MixinDeclaration])); - assert.ok(!hasParentsByType(node, [NodeType.Document])); - }); - - it('getChildByType', () => { - const ast = parseText([ - '$a: 1;', - '$b: 2;' - ]); - - assert.equal(getChildByType(ast, NodeType.VariableDeclaration).length, 2); - }); }); diff --git a/src/test/utils/document.spec.ts b/src/test/utils/document.spec.ts index 516cb89e..7f68a16c 100644 --- a/src/test/utils/document.spec.ts +++ b/src/test/utils/document.spec.ts @@ -2,50 +2,9 @@ import * as assert from 'assert'; -import { ISymbols } from '../../types/symbols'; -import { getCurrentDocumentImportPaths, getDocumentPath } from '../../utils/document'; +import { getDocumentPath } from '../../utils/document'; describe('Utils/Document', () => { - it('getCurrentDocumentImports', () => { - const symbolsList: ISymbols[] = [ - { - document: 'a.scss', - mixins: [], - functions: [], - variables: [], - imports: [ - { - filepath: 'b.scss', - css: false, - dynamic: false - } - ] - }, - { - document: 'b.scss', - mixins: [], - functions: [], - variables: [], - imports: [ - { - filepath: 'a.scss', - css: false, - dynamic: false - }, - { - filepath: 'c.scss', - css: false, - dynamic: false - } - ] - } - ]; - - const imports = getCurrentDocumentImportPaths(symbolsList, 'b.scss'); - - assert.equal(imports.length, 2); - }); - it('getDocumentPath', () => { assert.equal(getDocumentPath('test/file.scss', 'test/includes/a.scss'), 'includes/a.scss'); assert.equal(getDocumentPath('test/includes/a.scss', 'test/file.scss'), '../file.scss'); diff --git a/src/types/settings.ts b/src/types/settings.ts index 04e1fcab..97960569 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -7,7 +7,7 @@ export interface ISettings { scanImportedFiles: boolean; // Label - implicitlyLabel: string; + implicitlyLabel: string | null; // Display showErrors: boolean; diff --git a/src/utils/ast.ts b/src/utils/ast.ts index ce21d5e6..1a5fc5ba 100644 --- a/src/utils/ast.ts +++ b/src/utils/ast.ts @@ -28,7 +28,7 @@ export function getNodeAtOffset(parsedDocument: INode, posOffset: number): INode /** * Returns the parent Node of the specified type. */ -export function getParentNodeByType(node: INode, type: NodeType): INode { +export function getParentNodeByType(node: INode, type: NodeType): INode | null { node = node.getParent(); while (node.type !== type) { @@ -41,27 +41,3 @@ export function getParentNodeByType(node: INode, type: NodeType): INode { return node; } - -/** - * Returns True, if node has Parent with specified type(s). - */ -export function hasParentsByType(node: INode, types: NodeType[]): boolean { - node = node.getParent(); - - while (node.type !== NodeType.Stylesheet) { - if (types.indexOf(node.type) !== -1) { - return true; - } - - node = node.getParent(); - } - - return false; -} - -/** - * Returns the child Node of the specified type. - */ -export function getChildByType(parent: INode, type: NodeType): INode[] { - return parent.getChildren().filter(node => node.type === type); -} diff --git a/src/utils/document.ts b/src/utils/document.ts index c9ea122b..cf4c6881 100644 --- a/src/utils/document.ts +++ b/src/utils/document.ts @@ -2,21 +2,6 @@ import * as path from 'path'; -import { ISymbols } from '../types/symbols'; - -/** - * Returns imports for document. - */ -export function getCurrentDocumentImportPaths(symbolsList: ISymbols[], currentPath: string): string[] { - for (let i = 0; i < symbolsList.length; i++) { - if (symbolsList[i].document === currentPath) { - return symbolsList[i].imports.map(x => x.filepath); - } - } - - return []; -} - /** * Returns the path to the document, relative to the current document. */ diff --git a/src/utils/symbols.ts b/src/utils/symbols.ts index 67b2f356..d8debfb7 100644 --- a/src/utils/symbols.ts +++ b/src/utils/symbols.ts @@ -9,3 +9,7 @@ import StorageService from '../services/storage'; export function getSymbolsCollection(storage: StorageService): ISymbols[] { return storage.values(); } + +export function getSymbolsRelatedToDocument(storage: StorageService, current: string): ISymbols[] { + return getSymbolsCollection(storage).filter(item => item.document !== current || item.filepath !== current); +} diff --git a/yarn.lock b/yarn.lock index 6ab8c7e7..f53952f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1961,13 +1961,15 @@ vrsource-tslint-rules@^6.0.0: resolved "https://registry.yarnpkg.com/vrsource-tslint-rules/-/vrsource-tslint-rules-6.0.0.tgz#a4e25e8f3fdd487684174f423c090c35d60f37a9" integrity sha512-pmcnJdIVziZTk1V0Cqehmh3gIabBRkBYXkv9vx+1CZDNEa41kNGUBFwQLzw21erYOd2QnD8jJeZhBGqnlT1HWw== -vscode-css-languageservice@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-2.1.0.tgz#ad1a81e190b60c48bce9463ca1f7e90950c0a3fb" - integrity sha1-rRqB4ZC2DEi86UY8offpCVDAo/s= +vscode-css-languageservice@^4.0.3-next.23: + version "4.0.3-next.23" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.23.tgz#b7a835c317a6f0b96ec104e3ce168770f5f0d4ff" + integrity sha512-DmMXBzTd3uYnVMffNlcp+Sy4qdzeE+UG5yivo/5Y1f/Qr//4FyssH0eCZ7K9Vf/9DMKYf9J3HeCNZSTq4EzMrg== dependencies: - vscode-languageserver-types "^3.2.0" - vscode-nls "^2.0.1" + vscode-languageserver-textdocument "^1.0.0-next.4" + vscode-languageserver-types "^3.15.0-next.6" + vscode-nls "^4.1.1" + vscode-uri "^2.1.1" vscode-jsonrpc@^4.0.0: version "4.0.0" @@ -1990,11 +1992,21 @@ vscode-languageserver-protocol@3.14.1: vscode-jsonrpc "^4.0.0" vscode-languageserver-types "3.14.0" -vscode-languageserver-types@3.14.0, vscode-languageserver-types@^3.14.0, vscode-languageserver-types@^3.2.0: +vscode-languageserver-textdocument@^1.0.0-next.4: + version "1.0.0-next.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.0-next.5.tgz#dbb7a45dd973a19261a7c57ab9a439c40f3799ee" + integrity sha512-1jp/zAidN/bF/sqPimhBX1orH5G4rzRw63k75TesukJDuxm8yW79ECStWbDSy41BHGOwSGN4M69QFvhancSr5A== + +vscode-languageserver-types@3.14.0, vscode-languageserver-types@^3.14.0: version "3.14.0" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz#d3b5952246d30e5241592b6dde8280e03942e743" integrity sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A== +vscode-languageserver-types@^3.15.0-next.6: + version "3.15.0-next.8" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.15.0-next.8.tgz#59bfe70e5690bcef7d28d0f3a7f813915edf62e1" + integrity sha512-AEfWrSNyeamWMKPehh/kd3nBnKD9ZGCPhzfxMnW9YNqElSh28G2+Puk3knIQWyaWyV6Bzh28ok9BRJsPzXFCkQ== + vscode-languageserver@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-5.2.1.tgz#0d2feddd33f92aadf5da32450df498d52f6f14eb" @@ -2003,10 +2015,10 @@ vscode-languageserver@^5.2.1: vscode-languageserver-protocol "3.14.1" vscode-uri "^1.0.6" -vscode-nls@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-2.0.2.tgz#808522380844b8ad153499af5c3b03921aea02da" - integrity sha1-gIUiOAhEuK0VNJmvXDsDkhrqAto= +vscode-nls@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" + integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A== vscode-test@^1.2.3: version "1.2.3" @@ -2027,6 +2039,11 @@ vscode-uri@^1.0.6: resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.8.tgz#9769aaececae4026fb6e22359cb38946580ded59" integrity sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ== +vscode-uri@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.1.1.tgz#5aa1803391b6ebdd17d047f51365cf62c38f6e90" + integrity sha512-eY9jmGoEnVf8VE8xr5znSah7Qt1P/xsCdErz+g8HYZtJ7bZqKH5E3d+6oVNm1AC/c6IHUDokbmVXKOi4qPAC9A== + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"