From 31e02636ee088b439a01c58efa534745ba5bacfc Mon Sep 17 00:00:00 2001 From: Simon Stranks Date: Wed, 25 Oct 2023 00:08:58 +0100 Subject: [PATCH 01/14] init(various): setup mocha --- .mocharc.js | 9 + .mocharc.json | 6 - .vscode/OpenSourceContrib.code-workspace | 35 + .vscode/launch.json | 17 +- src/services/cssNavigation.ts | 1127 ++++++++++++---------- 5 files changed, 691 insertions(+), 503 deletions(-) create mode 100644 .mocharc.js delete mode 100644 .mocharc.json create mode 100644 .vscode/OpenSourceContrib.code-workspace diff --git a/.mocharc.js b/.mocharc.js new file mode 100644 index 00000000..f99c035d --- /dev/null +++ b/.mocharc.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + ui: 'tdd', + color: true, + // spec: './lib/umd/test/**/*.test.js', // aLl tests + spec: './lib/umd/test/css/navigation.test.js', // single test + recursive: true, +}; diff --git a/.mocharc.json b/.mocharc.json deleted file mode 100644 index fbf679e0..00000000 --- a/.mocharc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ui": "tdd", - "color": true, - "spec": "./lib/umd/test/**/*.test.js", - "recursive": true -} \ No newline at end of file diff --git a/.vscode/OpenSourceContrib.code-workspace b/.vscode/OpenSourceContrib.code-workspace new file mode 100644 index 00000000..e16b504a --- /dev/null +++ b/.vscode/OpenSourceContrib.code-workspace @@ -0,0 +1,35 @@ +{ + "folders": [ + { + "path": "../" + } + ], + + "settings": { + // ESLint looks to for 'import/resolver/' typescript; project. + // "eslint.workingDirectories": ["./frontend", "./backend"], + "workbench.editor.wrapTabs": false, + "workbench.startupEditor": "none", + "task.allowAutomaticTasks": "on" + // "path-intellisense.mappings": { + // "#Img": "${workspaceFolder}/frontend/src/assets/img", + // "#Sass": "${workspaceFolder}/frontend/src/assets/sass", + // "#Svg": "${workspaceFolder}/frontend/src/assets/svg", + // "#Components": "${workspaceFolder}/frontend/src/components", + // "#Context": "${workspaceFolder}/frontend/src/context", + // "#Data": "${workspaceFolder}/frontend/src/data", + // "#Feature": "${workspaceFolder}/frontend/src/features", + // "#Hooks": "${workspaceFolder}/frontend/src/hooks", + // "#Layouts": "${workspaceFolder}/frontend/src/layouts", + // "#Lib": "${workspaceFolder}/frontend/src/lib", + // "#Pages": "${workspaceFolder}/frontend/src/pages", + // "#Services": "${workspaceFolder}/frontend/src/services", + // "#Types": "${workspaceFolder}/frontend/src/types", + // "#Utils": "${workspaceFolder}/frontend/src/utils" + // } + }, + "launch": { + "version": "0.2.0", + "configurations": [] + } +} diff --git a/.vscode/launch.json b/.vscode/launch.json index a45fb872..81d5f99e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,27 +4,20 @@ { "name": "Unit Tests", "type": "node", + "runtimeVersion": "18.16.0", "request": "launch", "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", "stopOnEntry": false, - "args": [ - "--timeout", - "999999", - "--colors" - ], + "args": ["--timeout", "999999", "--colors"], // in .mocharc.js "cwd": "${workspaceRoot}", "runtimeExecutable": null, "runtimeArgs": [], "env": {}, "sourceMaps": true, - "outFiles": [ - "${workspaceRoot}/lib/umd/**" - ], - "skipFiles": [ - "/**" - ], + "outFiles": ["${workspaceRoot}/lib/umd/**"], + "skipFiles": ["/**"], "smartStep": true, "preLaunchTask": "npm: watch" } ] -} \ No newline at end of file +} diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index 9b52c698..bceb7fb8 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -4,522 +4,679 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import * as l10n from '@vscode/l10n'; import { - Color, ColorInformation, ColorPresentation, DocumentHighlight, DocumentHighlightKind, DocumentLink, Location, - Position, Range, SymbolInformation, SymbolKind, TextEdit, WorkspaceEdit, TextDocument, DocumentContext, FileSystemProvider, FileType, DocumentSymbol + Color, + ColorInformation, + ColorPresentation, + DocumentContext, + DocumentHighlight, + DocumentHighlightKind, + DocumentLink, + DocumentSymbol, + FileSystemProvider, + FileType, + Location, + Position, + Range, + SymbolInformation, + SymbolKind, + TextDocument, + TextEdit, + WorkspaceEdit, } from '../cssLanguageTypes'; -import * as l10n from '@vscode/l10n'; +import { getColorValue, hslFromColor, hwbFromColor } from '../languageFacts/facts'; import * as nodes from '../parser/cssNodes'; import { Symbols } from '../parser/cssSymbolScope'; -import { getColorValue, hslFromColor, hwbFromColor } from '../languageFacts/facts'; -import { startsWith } from '../utils/strings'; import { dirname, joinPath } from '../utils/resources'; +import { startsWith } from '../utils/strings'; +type UnresolvedLinkData = { link: DocumentLink; isRawLink: boolean }; -type UnresolvedLinkData = { link: DocumentLink, isRawLink: boolean }; - -type DocumentSymbolCollector = (name: string, kind: SymbolKind, symbolNodeOrRange: nodes.Node | Range, nameNodeOrRange: nodes.Node | Range | undefined, bodyNode: nodes.Node | undefined) => void; +type DocumentSymbolCollector = ( + name: string, + kind: SymbolKind, + symbolNodeOrRange: nodes.Node | Range, + nameNodeOrRange: nodes.Node | Range | undefined, + bodyNode: nodes.Node | undefined +) => void; const startsWithSchemeRegex = /^\w+:\/\//; const startsWithData = /^data:/; export class CSSNavigation { - - constructor(protected fileSystemProvider: FileSystemProvider | undefined, private readonly resolveModuleReferences: boolean) { - } - - public findDefinition(document: TextDocument, position: Position, stylesheet: nodes.Node): Location | null { - - const symbols = new Symbols(stylesheet); - const offset = document.offsetAt(position); - const node = nodes.getNodeAtOffset(stylesheet, offset); - - if (!node) { - return null; - } - - const symbol = symbols.findSymbolFromNode(node); - if (!symbol) { - return null; - } - - return { - uri: document.uri, - range: getRange(symbol.node, document) - }; - } - - public findReferences(document: TextDocument, position: Position, stylesheet: nodes.Stylesheet): Location[] { - const highlights = this.findDocumentHighlights(document, position, stylesheet); - return highlights.map(h => { - return { - uri: document.uri, - range: h.range - }; - }); - } - - private getHighlightNode(document: TextDocument, position: Position, stylesheet: nodes.Stylesheet): nodes.Node | undefined { - const offset = document.offsetAt(position); - let node = nodes.getNodeAtOffset(stylesheet, offset); - if (!node || node.type === nodes.NodeType.Stylesheet || node.type === nodes.NodeType.Declarations) { - return; - } - if (node.type === nodes.NodeType.Identifier && node.parent && node.parent.type === nodes.NodeType.ClassSelector) { - node = node.parent; - } - - return node; - } - - public findDocumentHighlights(document: TextDocument, position: Position, stylesheet: nodes.Stylesheet): DocumentHighlight[] { - const result: DocumentHighlight[] = []; - const node = this.getHighlightNode(document, position, stylesheet); - if (!node) { - return result; - } - - const symbols = new Symbols(stylesheet); - const symbol = symbols.findSymbolFromNode(node); - const name = node.getText(); - - stylesheet.accept(candidate => { - if (symbol) { - if (symbols.matchesSymbol(candidate, symbol)) { - result.push({ - kind: getHighlightKind(candidate), - range: getRange(candidate, document) - }); - return false; - } - } else if (node && node.type === candidate.type && candidate.matches(name)) { - // Same node type and data - result.push({ - kind: getHighlightKind(candidate), - range: getRange(candidate, document) - }); - } - return true; - }); - - return result; - } - - protected isRawStringDocumentLinkNode(node: nodes.Node): boolean { - return node.type === nodes.NodeType.Import; - } - - public findDocumentLinks(document: TextDocument, stylesheet: nodes.Stylesheet, documentContext: DocumentContext): DocumentLink[] { - const linkData = this.findUnresolvedLinks(document, stylesheet); - const resolvedLinks: DocumentLink[] = []; - for (let data of linkData) { - const link = data.link; - const target = link.target; - if (!target || startsWithData.test(target)) { - // no links for data: - } else if (startsWithSchemeRegex.test(target)) { - resolvedLinks.push(link); - } else { - const resolved = documentContext.resolveReference(target, document.uri); - if (resolved) { - link.target = resolved; - } - resolvedLinks.push(link); - } - } - return resolvedLinks; - } - - public async findDocumentLinks2(document: TextDocument, stylesheet: nodes.Stylesheet, documentContext: DocumentContext): Promise { - const linkData = this.findUnresolvedLinks(document, stylesheet); - const resolvedLinks: DocumentLink[] = []; - for (let data of linkData) { - const link = data.link; - const target = link.target; - if (!target || startsWithData.test(target)) { - // no links for data: - } else if (startsWithSchemeRegex.test(target)) { - resolvedLinks.push(link); - } else { - const resolvedTarget = await this.resolveReference(target, document.uri, documentContext, data.isRawLink); - if (resolvedTarget !== undefined) { - link.target = resolvedTarget; - resolvedLinks.push(link); - } - } - } - return resolvedLinks; - } - - - private findUnresolvedLinks(document: TextDocument, stylesheet: nodes.Stylesheet): UnresolvedLinkData[] { - const result: UnresolvedLinkData[] = []; - - const collect = (uriStringNode: nodes.Node) => { - let rawUri = uriStringNode.getText(); - const range = getRange(uriStringNode, document); - // Make sure the range is not empty - if (range.start.line === range.end.line && range.start.character === range.end.character) { - return; - } - - if (startsWith(rawUri, `'`) || startsWith(rawUri, `"`)) { - rawUri = rawUri.slice(1, -1); - } - - const isRawLink = uriStringNode.parent ? this.isRawStringDocumentLinkNode(uriStringNode.parent) : false; - result.push({ link: { target: rawUri, range }, isRawLink }); - }; - - stylesheet.accept(candidate => { - if (candidate.type === nodes.NodeType.URILiteral) { - const first = candidate.getChild(0); - if (first) { - collect(first); - } - return false; - } - - /** - * In @import, it is possible to include links that do not use `url()` - * For example, `@import 'foo.css';` - */ - if (candidate.parent && this.isRawStringDocumentLinkNode(candidate.parent)) { - const rawText = candidate.getText(); - if (startsWith(rawText, `'`) || startsWith(rawText, `"`)) { - collect(candidate); - } - return false; - } - - return true; - }); - - return result; - } - - public findSymbolInformations(document: TextDocument, stylesheet: nodes.Stylesheet): SymbolInformation[] { - - const result: SymbolInformation[] = []; - - const addSymbolInformation = (name: string, kind: SymbolKind, symbolNodeOrRange: nodes.Node | Range) => { - const range = symbolNodeOrRange instanceof nodes.Node ? getRange(symbolNodeOrRange, document) : symbolNodeOrRange; - const entry: SymbolInformation = { - name: name || l10n.t(''), - kind, - location: Location.create(document.uri, range) - }; - result.push(entry); - }; - - this.collectDocumentSymbols(document, stylesheet, addSymbolInformation); - - return result; - } - - public findDocumentSymbols(document: TextDocument, stylesheet: nodes.Stylesheet): DocumentSymbol[] { - const result: DocumentSymbol[] = []; - - const parents: [DocumentSymbol, Range][] = []; - - const addDocumentSymbol = (name: string, kind: SymbolKind, symbolNodeOrRange: nodes.Node | Range, nameNodeOrRange: nodes.Node | Range | undefined, bodyNode: nodes.Node | undefined) => { - const range = symbolNodeOrRange instanceof nodes.Node ? getRange(symbolNodeOrRange, document) : symbolNodeOrRange; - let selectionRange = nameNodeOrRange instanceof nodes.Node ? getRange(nameNodeOrRange, document) : nameNodeOrRange; - if (!selectionRange || !containsRange(range, selectionRange)) { - selectionRange = Range.create(range.start, range.start); - } - - const entry: DocumentSymbol = { - name: name || l10n.t(''), - kind, - range, - selectionRange - }; - let top = parents.pop(); - while (top && !containsRange(top[1], range)) { - top = parents.pop(); - } - if (top) { - const topSymbol = top[0]; - if (!topSymbol.children) { - topSymbol.children = []; - } - topSymbol.children.push(entry); - parents.push(top); // put back top - } else { - result.push(entry); - } - if (bodyNode) { - parents.push([entry, getRange(bodyNode, document)]); - } - }; - - this.collectDocumentSymbols(document, stylesheet, addDocumentSymbol); - - return result; - } - - private collectDocumentSymbols(document: TextDocument, stylesheet: nodes.Stylesheet, collect: DocumentSymbolCollector): void { - stylesheet.accept(node => { - if (node instanceof nodes.RuleSet) { - for (const selector of node.getSelectors().getChildren()) { - if (selector instanceof nodes.Selector) { - const range = Range.create(document.positionAt(selector.offset), document.positionAt(node.end)); - collect(selector.getText(), SymbolKind.Class, range, selector, node.getDeclarations()); - } - } - } else if (node instanceof nodes.VariableDeclaration) { - collect(node.getName(), SymbolKind.Variable, node, node.getVariable(), undefined); - } else if (node instanceof nodes.MixinDeclaration) { - collect(node.getName(), SymbolKind.Method, node, node.getIdentifier(), node.getDeclarations()); - } else if (node instanceof nodes.FunctionDeclaration) { - collect(node.getName(), SymbolKind.Function, node, node.getIdentifier(), node.getDeclarations()); - } else if (node instanceof nodes.Keyframe) { - const name = l10n.t("@keyframes {0}", node.getName()); - collect(name, SymbolKind.Class, node, node.getIdentifier(), node.getDeclarations()); - } else if (node instanceof nodes.FontFace) { - const name = l10n.t("@font-face"); - collect(name, SymbolKind.Class, node, undefined, node.getDeclarations()); - } else if (node instanceof nodes.Media) { - const mediaList = node.getChild(0); - if (mediaList instanceof nodes.Medialist) { - const name = '@media ' + mediaList.getText(); - collect(name, SymbolKind.Module, node, mediaList, node.getDeclarations()); - } - } - return true; - }); - } - - public findDocumentColors(document: TextDocument, stylesheet: nodes.Stylesheet): ColorInformation[] { - const result: ColorInformation[] = []; - stylesheet.accept((node) => { - const colorInfo = getColorInformation(node, document); - if (colorInfo) { - result.push(colorInfo); - } - return true; - }); - return result; - } - - public getColorPresentations(document: TextDocument, stylesheet: nodes.Stylesheet, color: Color, range: Range): ColorPresentation[] { - const result: ColorPresentation[] = []; - const red256 = Math.round(color.red * 255), green256 = Math.round(color.green * 255), blue256 = Math.round(color.blue * 255); - - let label; - if (color.alpha === 1) { - label = `rgb(${red256}, ${green256}, ${blue256})`; - } else { - label = `rgba(${red256}, ${green256}, ${blue256}, ${color.alpha})`; - } - result.push({ label: label, textEdit: TextEdit.replace(range, label) }); - - if (color.alpha === 1) { - label = `#${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex(blue256)}`; - } else { - label = `#${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex(blue256)}${toTwoDigitHex(Math.round(color.alpha * 255))}`; - } - result.push({ label: label, textEdit: TextEdit.replace(range, label) }); - - const hsl = hslFromColor(color); - if (hsl.a === 1) { - label = `hsl(${hsl.h}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%)`; - } else { - label = `hsla(${hsl.h}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%, ${hsl.a})`; - } - result.push({ label: label, textEdit: TextEdit.replace(range, label) }); - - const hwb = hwbFromColor(color); - if (hwb.a === 1) { - label = `hwb(${hwb.h} ${Math.round(hwb.w * 100)}% ${Math.round(hwb.b * 100)}%)`; - } else { - label = `hwb(${hwb.h} ${Math.round(hwb.w * 100)}% ${Math.round(hwb.b * 100)}% / ${hwb.a})`; - } - result.push({ label: label, textEdit: TextEdit.replace(range, label) }); - - return result; - } - - public prepareRename(document: TextDocument, position: Position, stylesheet: nodes.Stylesheet): Range | undefined { - const node = this.getHighlightNode(document, position, stylesheet); - if (node) { - return Range.create(document.positionAt(node.offset), document.positionAt(node.end)); - } - } - - public doRename(document: TextDocument, position: Position, newName: string, stylesheet: nodes.Stylesheet): WorkspaceEdit { - const highlights = this.findDocumentHighlights(document, position, stylesheet); - const edits = highlights.map(h => TextEdit.replace(h.range, newName)); - return { - changes: { [document.uri]: edits } - }; - } - - protected async resolveModuleReference(ref: string, documentUri: string, documentContext: DocumentContext): Promise { - if (startsWith(documentUri, 'file://')) { - const moduleName = getModuleNameFromPath(ref); - if (moduleName && moduleName !== '.' && moduleName !== '..') { - const rootFolderUri = documentContext.resolveReference('/', documentUri); - const documentFolderUri = dirname(documentUri); - const modulePath = await this.resolvePathToModule(moduleName, documentFolderUri, rootFolderUri); - if (modulePath) { - const pathWithinModule = ref.substring(moduleName.length + 1); - return joinPath(modulePath, pathWithinModule); - } - } - } - return undefined; - } - - protected async mapReference(target: string | undefined, isRawLink: boolean): Promise { - return target; - } - - protected async resolveReference(target: string, documentUri: string, documentContext: DocumentContext, isRawLink = false): Promise { - - // Following [css-loader](https://github.com/webpack-contrib/css-loader#url) - // and [sass-loader's](https://github.com/webpack-contrib/sass-loader#imports) - // convention, if an import path starts with ~ then use node module resolution - // *unless* it starts with "~/" as this refers to the user's home directory. - if (target[0] === '~' && target[1] !== '/' && this.fileSystemProvider) { - target = target.substring(1); - return this.mapReference(await this.resolveModuleReference(target, documentUri, documentContext), isRawLink); - } - - const ref = await this.mapReference(documentContext.resolveReference(target, documentUri), isRawLink); - - // Following [less-loader](https://github.com/webpack-contrib/less-loader#imports) - // and [sass-loader's](https://github.com/webpack-contrib/sass-loader#resolving-import-at-rules) - // new resolving import at-rules (~ is deprecated). The loader will first try to resolve @import as a relative path. If it cannot be resolved, - // then the loader will try to resolve @import inside node_modules. - if (this.resolveModuleReferences) { - if (ref && await this.fileExists(ref)) { - return ref; - } - - const moduleReference = await this.mapReference(await this.resolveModuleReference(target, documentUri, documentContext), isRawLink); - if (moduleReference) { - return moduleReference; - } - } - // fall back. it might not exists - return ref; - } - - private async resolvePathToModule(_moduleName: string, documentFolderUri: string, rootFolderUri: string | undefined): Promise { - // resolve the module relative to the document. We can't use `require` here as the code is webpacked. - - const packPath = joinPath(documentFolderUri, 'node_modules', _moduleName, 'package.json'); - if (await this.fileExists(packPath)) { - return dirname(packPath); - } else if (rootFolderUri && documentFolderUri.startsWith(rootFolderUri) && (documentFolderUri.length !== rootFolderUri.length)) { - return this.resolvePathToModule(_moduleName, dirname(documentFolderUri), rootFolderUri); - } - return undefined; - } - - protected async fileExists(uri: string): Promise { - if (!this.fileSystemProvider) { - return false; - } - try { - const stat = await this.fileSystemProvider.stat(uri); - if (stat.type === FileType.Unknown && stat.size === -1) { - return false; - } - - return true; - } catch (err) { - return false; - } - } - + constructor( + protected fileSystemProvider: FileSystemProvider | undefined, + private readonly resolveModuleReferences: boolean + ) {} + + public findDefinition( + document: TextDocument, + position: Position, + stylesheet: nodes.Node + ): Location | null { + const symbols = new Symbols(stylesheet); + const offset = document.offsetAt(position); + const node = nodes.getNodeAtOffset(stylesheet, offset); + + if (!node) { + return null; + } + + const symbol = symbols.findSymbolFromNode(node); + if (!symbol) { + return null; + } + + return { + uri: document.uri, + range: getRange(symbol.node, document), + }; + } + + public findReferences( + document: TextDocument, + position: Position, + stylesheet: nodes.Stylesheet + ): Location[] { + const highlights = this.findDocumentHighlights(document, position, stylesheet); + return highlights.map((h) => { + return { + uri: document.uri, + range: h.range, + }; + }); + } + + private getHighlightNode( + document: TextDocument, + position: Position, + stylesheet: nodes.Stylesheet + ): nodes.Node | undefined { + const offset = document.offsetAt(position); + let node = nodes.getNodeAtOffset(stylesheet, offset); + if ( + !node || + node.type === nodes.NodeType.Stylesheet || + node.type === nodes.NodeType.Declarations + ) { + return; + } + if ( + node.type === nodes.NodeType.Identifier && + node.parent && + node.parent.type === nodes.NodeType.ClassSelector + ) { + node = node.parent; + } + + return node; + } + + public findDocumentHighlights( + document: TextDocument, + position: Position, + stylesheet: nodes.Stylesheet + ): DocumentHighlight[] { + const result: DocumentHighlight[] = []; + const node = this.getHighlightNode(document, position, stylesheet); + if (!node) { + return result; + } + + const symbols = new Symbols(stylesheet); + const symbol = symbols.findSymbolFromNode(node); + const name = node.getText(); + + stylesheet.accept((candidate) => { + if (symbol) { + if (symbols.matchesSymbol(candidate, symbol)) { + result.push({ + kind: getHighlightKind(candidate), + range: getRange(candidate, document), + }); + return false; + } + } else if (node && node.type === candidate.type && candidate.matches(name)) { + // Same node type and data + result.push({ + kind: getHighlightKind(candidate), + range: getRange(candidate, document), + }); + } + return true; + }); + + return result; + } + + protected isRawStringDocumentLinkNode(node: nodes.Node): boolean { + return node.type === nodes.NodeType.Import; + } + + public findDocumentLinks( + document: TextDocument, + stylesheet: nodes.Stylesheet, + documentContext: DocumentContext + ): DocumentLink[] { + const linkData = this.findUnresolvedLinks(document, stylesheet); + const resolvedLinks: DocumentLink[] = []; + for (let data of linkData) { + const link = data.link; + const target = link.target; + if (!target || startsWithData.test(target)) { + // no links for data: + } else if (startsWithSchemeRegex.test(target)) { + resolvedLinks.push(link); + } else { + const resolved = documentContext.resolveReference(target, document.uri); + if (resolved) { + link.target = resolved; + } + resolvedLinks.push(link); + } + } + return resolvedLinks; + } + + public async findDocumentLinks2( + document: TextDocument, + stylesheet: nodes.Stylesheet, + documentContext: DocumentContext + ): Promise { + const linkData = this.findUnresolvedLinks(document, stylesheet); + const resolvedLinks: DocumentLink[] = []; + for (let data of linkData) { + const link = data.link; + const target = link.target; + if (!target || startsWithData.test(target)) { + // no links for data: + } else if (startsWithSchemeRegex.test(target)) { + resolvedLinks.push(link); + } else { + const resolvedTarget = await this.resolveReference( + target, + document.uri, + documentContext, + data.isRawLink + ); + if (resolvedTarget !== undefined) { + link.target = resolvedTarget; + resolvedLinks.push(link); + } + } + } + return resolvedLinks; + } + + private findUnresolvedLinks( + document: TextDocument, + stylesheet: nodes.Stylesheet + ): UnresolvedLinkData[] { + const result: UnresolvedLinkData[] = []; + + const collect = (uriStringNode: nodes.Node) => { + let rawUri = uriStringNode.getText(); + const range = getRange(uriStringNode, document); + // Make sure the range is not empty + if (range.start.line === range.end.line && range.start.character === range.end.character) { + return; + } + + if (startsWith(rawUri, `'`) || startsWith(rawUri, `"`)) { + rawUri = rawUri.slice(1, -1); + } + + const isRawLink = uriStringNode.parent + ? this.isRawStringDocumentLinkNode(uriStringNode.parent) + : false; + result.push({ link: { target: rawUri, range }, isRawLink }); + }; + + stylesheet.accept((candidate) => { + if (candidate.type === nodes.NodeType.URILiteral) { + const first = candidate.getChild(0); + if (first) { + collect(first); + } + return false; + } + + /** + * In @import, it is possible to include links that do not use `url()` + * For example, `@import 'foo.css';` + */ + if (candidate.parent && this.isRawStringDocumentLinkNode(candidate.parent)) { + const rawText = candidate.getText(); + if (startsWith(rawText, `'`) || startsWith(rawText, `"`)) { + collect(candidate); + } + return false; + } + + return true; + }); + + return result; + } + + public findSymbolInformations( + document: TextDocument, + stylesheet: nodes.Stylesheet + ): SymbolInformation[] { + const result: SymbolInformation[] = []; + + const addSymbolInformation = ( + name: string, + kind: SymbolKind, + symbolNodeOrRange: nodes.Node | Range + ) => { + const range = + symbolNodeOrRange instanceof nodes.Node + ? getRange(symbolNodeOrRange, document) + : symbolNodeOrRange; + const entry: SymbolInformation = { + name: name || l10n.t(''), + kind, + location: Location.create(document.uri, range), + }; + result.push(entry); + }; + + this.collectDocumentSymbols(document, stylesheet, addSymbolInformation); + + return result; + } + + public findDocumentSymbols( + document: TextDocument, + stylesheet: nodes.Stylesheet + ): DocumentSymbol[] { + const result: DocumentSymbol[] = []; + + const parents: [DocumentSymbol, Range][] = []; + + const addDocumentSymbol = ( + name: string, + kind: SymbolKind, + symbolNodeOrRange: nodes.Node | Range, + nameNodeOrRange: nodes.Node | Range | undefined, + bodyNode: nodes.Node | undefined + ) => { + const range = + symbolNodeOrRange instanceof nodes.Node + ? getRange(symbolNodeOrRange, document) + : symbolNodeOrRange; + let selectionRange = + nameNodeOrRange instanceof nodes.Node + ? getRange(nameNodeOrRange, document) + : nameNodeOrRange; + if (!selectionRange || !containsRange(range, selectionRange)) { + selectionRange = Range.create(range.start, range.start); + } + + const entry: DocumentSymbol = { + name: name || l10n.t(''), + kind, + range, + selectionRange, + }; + let top = parents.pop(); + while (top && !containsRange(top[1], range)) { + top = parents.pop(); + } + if (top) { + const topSymbol = top[0]; + if (!topSymbol.children) { + topSymbol.children = []; + } + topSymbol.children.push(entry); + parents.push(top); // put back top + } else { + result.push(entry); + } + if (bodyNode) { + parents.push([entry, getRange(bodyNode, document)]); + } + }; + + this.collectDocumentSymbols(document, stylesheet, addDocumentSymbol); + + return result; + } + + private collectDocumentSymbols( + document: TextDocument, + stylesheet: nodes.Stylesheet, + collect: DocumentSymbolCollector + ): void { + stylesheet.accept((node) => { + if (node instanceof nodes.RuleSet) { + for (const selector of node.getSelectors().getChildren()) { + if (selector instanceof nodes.Selector) { + const range = Range.create( + document.positionAt(selector.offset), + document.positionAt(node.end) + ); + collect(selector.getText(), SymbolKind.Class, range, selector, node.getDeclarations()); + } + } + } else if (node instanceof nodes.VariableDeclaration) { + collect(node.getName(), SymbolKind.Variable, node, node.getVariable(), undefined); + } else if (node instanceof nodes.MixinDeclaration) { + collect( + node.getName(), + SymbolKind.Method, + node, + node.getIdentifier(), + node.getDeclarations() + ); + } else if (node instanceof nodes.FunctionDeclaration) { + collect( + node.getName(), + SymbolKind.Function, + node, + node.getIdentifier(), + node.getDeclarations() + ); + } else if (node instanceof nodes.Keyframe) { + const name = l10n.t('@keyframes {0}', node.getName()); + collect(name, SymbolKind.Class, node, node.getIdentifier(), node.getDeclarations()); + } else if (node instanceof nodes.FontFace) { + const name = l10n.t('@font-face'); + collect(name, SymbolKind.Class, node, undefined, node.getDeclarations()); + } else if (node instanceof nodes.Media) { + const mediaList = node.getChild(0); + if (mediaList instanceof nodes.Medialist) { + const name = '@media ' + mediaList.getText(); + collect(name, SymbolKind.Module, node, mediaList, node.getDeclarations()); + } + } + return true; + }); + } + + public findDocumentColors( + document: TextDocument, + stylesheet: nodes.Stylesheet + ): ColorInformation[] { + const result: ColorInformation[] = []; + stylesheet.accept((node) => { + const colorInfo = getColorInformation(node, document); + if (colorInfo) { + result.push(colorInfo); + } + return true; + }); + return result; + } + + public getColorPresentations( + document: TextDocument, + stylesheet: nodes.Stylesheet, + color: Color, + range: Range + ): ColorPresentation[] { + const result: ColorPresentation[] = []; + const red256 = Math.round(color.red * 255), + green256 = Math.round(color.green * 255), + blue256 = Math.round(color.blue * 255); + + let label; + if (color.alpha === 1) { + label = `rgb(${red256}, ${green256}, ${blue256})`; + } else { + label = `rgba(${red256}, ${green256}, ${blue256}, ${color.alpha})`; + } + result.push({ label: label, textEdit: TextEdit.replace(range, label) }); + + if (color.alpha === 1) { + label = `#${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex(blue256)}`; + } else { + label = `#${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex( + blue256 + )}${toTwoDigitHex(Math.round(color.alpha * 255))}`; + } + result.push({ label: label, textEdit: TextEdit.replace(range, label) }); + + const hsl = hslFromColor(color); + if (hsl.a === 1) { + label = `hsl(${hsl.h}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%)`; + } else { + label = `hsla(${hsl.h}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%, ${hsl.a})`; + } + result.push({ label: label, textEdit: TextEdit.replace(range, label) }); + + const hwb = hwbFromColor(color); + if (hwb.a === 1) { + label = `hwb(${hwb.h} ${Math.round(hwb.w * 100)}% ${Math.round(hwb.b * 100)}%)`; + } else { + label = `hwb(${hwb.h} ${Math.round(hwb.w * 100)}% ${Math.round(hwb.b * 100)}% / ${hwb.a})`; + } + result.push({ label: label, textEdit: TextEdit.replace(range, label) }); + + return result; + } + + public prepareRename( + document: TextDocument, + position: Position, + stylesheet: nodes.Stylesheet + ): Range | undefined { + const node = this.getHighlightNode(document, position, stylesheet); + if (node) { + return Range.create(document.positionAt(node.offset), document.positionAt(node.end)); + } + } + + public doRename( + document: TextDocument, + position: Position, + newName: string, + stylesheet: nodes.Stylesheet + ): WorkspaceEdit { + const highlights = this.findDocumentHighlights(document, position, stylesheet); + const edits = highlights.map((h) => TextEdit.replace(h.range, newName)); + return { + changes: { [document.uri]: edits }, + }; + } + + protected async resolveModuleReference( + ref: string, + documentUri: string, + documentContext: DocumentContext + ): Promise { + if (startsWith(documentUri, 'file://')) { + const moduleName = getModuleNameFromPath(ref); + if (moduleName && moduleName !== '.' && moduleName !== '..') { + const rootFolderUri = documentContext.resolveReference('/', documentUri); + const documentFolderUri = dirname(documentUri); + const modulePath = await this.resolvePathToModule( + moduleName, + documentFolderUri, + rootFolderUri + ); + if (modulePath) { + const pathWithinModule = ref.substring(moduleName.length + 1); + return joinPath(modulePath, pathWithinModule); + } + } + } + return undefined; + } + + protected async mapReference( + target: string | undefined, + isRawLink: boolean + ): Promise { + return target; + } + + protected async resolveReference( + target: string, + documentUri: string, + documentContext: DocumentContext, + isRawLink = false + ): Promise { + // Following [css-loader](https://github.com/webpack-contrib/css-loader#url) + // and [sass-loader's](https://github.com/webpack-contrib/sass-loader#imports) + // convention, if an import path starts with ~ then use node module resolution + // *unless* it starts with "~/" as this refers to the user's home directory. + if (target[0] === '~' && target[1] !== '/' && this.fileSystemProvider) { + target = target.substring(1); + return this.mapReference( + await this.resolveModuleReference(target, documentUri, documentContext), + isRawLink + ); + } + + const ref = await this.mapReference( + documentContext.resolveReference(target, documentUri), + isRawLink + ); + + // Following [less-loader](https://github.com/webpack-contrib/less-loader#imports) + // and [sass-loader's](https://github.com/webpack-contrib/sass-loader#resolving-import-at-rules) + // new resolving import at-rules (~ is deprecated). The loader will first try to resolve @import as a relative path. If it cannot be resolved, + // then the loader will try to resolve @import inside node_modules. + if (this.resolveModuleReferences) { + if (ref && (await this.fileExists(ref))) { + return ref; + } + + const moduleReference = await this.mapReference( + await this.resolveModuleReference(target, documentUri, documentContext), + isRawLink + ); + if (moduleReference) { + return moduleReference; + } + } + // fall back. it might not exists + return ref; + } + + private async resolvePathToModule( + _moduleName: string, + documentFolderUri: string, + rootFolderUri: string | undefined + ): Promise { + // resolve the module relative to the document. We can't use `require` here as the code is webpacked. + + const packPath = joinPath(documentFolderUri, 'node_modules', _moduleName, 'package.json'); + if (await this.fileExists(packPath)) { + return dirname(packPath); + } else if ( + rootFolderUri && + documentFolderUri.startsWith(rootFolderUri) && + documentFolderUri.length !== rootFolderUri.length + ) { + return this.resolvePathToModule(_moduleName, dirname(documentFolderUri), rootFolderUri); + } + return undefined; + } + + protected async fileExists(uri: string): Promise { + if (!this.fileSystemProvider) { + return false; + } + try { + const stat = await this.fileSystemProvider.stat(uri); + if (stat.type === FileType.Unknown && stat.size === -1) { + return false; + } + + return true; + } catch (err) { + return false; + } + } } function getColorInformation(node: nodes.Node, document: TextDocument): ColorInformation | null { - const color = getColorValue(node); - if (color) { - const range = getRange(node, document); - return { color, range }; - } - return null; + const color = getColorValue(node); + if (color) { + const range = getRange(node, document); + return { color, range }; + } + return null; } - function getRange(node: nodes.Node, document: TextDocument): Range { - return Range.create(document.positionAt(node.offset), document.positionAt(node.end)); + return Range.create(document.positionAt(node.offset), document.positionAt(node.end)); } /** * Test if `otherRange` is in `range`. If the ranges are equal, will return true. */ function containsRange(range: Range, otherRange: Range): boolean { - const otherStartLine = otherRange.start.line, otherEndLine = otherRange.end.line; - const rangeStartLine = range.start.line, rangeEndLine = range.end.line; - - if (otherStartLine < rangeStartLine || otherEndLine < rangeStartLine) { - return false; - } - if (otherStartLine > rangeEndLine || otherEndLine > rangeEndLine) { - return false; - } - if (otherStartLine === rangeStartLine && otherRange.start.character < range.start.character) { - return false; - } - if (otherEndLine === rangeEndLine && otherRange.end.character > range.end.character) { - return false; - } - return true; + const otherStartLine = otherRange.start.line, + otherEndLine = otherRange.end.line; + const rangeStartLine = range.start.line, + rangeEndLine = range.end.line; + + if (otherStartLine < rangeStartLine || otherEndLine < rangeStartLine) { + return false; + } + if (otherStartLine > rangeEndLine || otherEndLine > rangeEndLine) { + return false; + } + if (otherStartLine === rangeStartLine && otherRange.start.character < range.start.character) { + return false; + } + if (otherEndLine === rangeEndLine && otherRange.end.character > range.end.character) { + return false; + } + return true; } function getHighlightKind(node: nodes.Node): DocumentHighlightKind { - - if (node.type === nodes.NodeType.Selector) { - return DocumentHighlightKind.Write; - } - - if (node instanceof nodes.Identifier) { - if (node.parent && node.parent instanceof nodes.Property) { - if (node.isCustomProperty) { - return DocumentHighlightKind.Write; - } - } - } - - if (node.parent) { - switch (node.parent.type) { - case nodes.NodeType.FunctionDeclaration: - case nodes.NodeType.MixinDeclaration: - case nodes.NodeType.Keyframe: - case nodes.NodeType.VariableDeclaration: - case nodes.NodeType.FunctionParameter: - return DocumentHighlightKind.Write; - } - } - - return DocumentHighlightKind.Read; + if (node.type === nodes.NodeType.Selector) { + return DocumentHighlightKind.Write; + } + + if (node instanceof nodes.Identifier) { + if (node.parent && node.parent instanceof nodes.Property) { + if (node.isCustomProperty) { + return DocumentHighlightKind.Write; + } + } + } + + if (node.parent) { + switch (node.parent.type) { + case nodes.NodeType.FunctionDeclaration: + case nodes.NodeType.MixinDeclaration: + case nodes.NodeType.Keyframe: + case nodes.NodeType.VariableDeclaration: + case nodes.NodeType.FunctionParameter: + return DocumentHighlightKind.Write; + } + } + + return DocumentHighlightKind.Read; } function toTwoDigitHex(n: number): string { - const r = n.toString(16); - return r.length !== 2 ? '0' + r : r; + const r = n.toString(16); + return r.length !== 2 ? '0' + r : r; } function getModuleNameFromPath(path: string) { - const firstSlash = path.indexOf('/'); - if (firstSlash === -1) { - return ''; - } - - // If a scoped module (starts with @) then get up until second instance of '/', or to the end of the string for root-level imports. - if (path[0] === '@') { - const secondSlash = path.indexOf('/', firstSlash + 1); - if (secondSlash === -1) { - return path; - } - return path.substring(0, secondSlash); - } - // Otherwise get until first instance of '/' - return path.substring(0, firstSlash); + const firstSlash = path.indexOf('/'); + if (firstSlash === -1) { + return ''; + } + + // If a scoped module (starts with @) then get up until second instance of '/', or to the end of the string for root-level imports. + if (path[0] === '@') { + const secondSlash = path.indexOf('/', firstSlash + 1); + if (secondSlash === -1) { + return path; + } + return path.substring(0, secondSlash); + } + // Otherwise get until first instance of '/' + return path.substring(0, firstSlash); } From 4f56e2f1971f276fbef091ac80ffe350b7cf0bc1 Mon Sep 17 00:00:00 2001 From: Simon Stranks Date: Sat, 28 Oct 2023 00:22:46 +0100 Subject: [PATCH 02/14] fix(css reference links): wip --- src/cssLanguageService.ts | 1 + src/cssLanguageTypes.ts | 6 ++ src/services/cssNavigation.ts | 115 ++++++++++++++++++---------------- 3 files changed, 67 insertions(+), 55 deletions(-) diff --git a/src/cssLanguageService.ts b/src/cssLanguageService.ts index a9d9b676..e175ec6a 100644 --- a/src/cssLanguageService.ts +++ b/src/cssLanguageService.ts @@ -80,6 +80,7 @@ function createFacade(parser: Parser, completion: CSSCompletion, hover: CSSHover validation.configure(settings); completion.configure(settings?.completion); hover.configure(settings?.hover); + navigation.configure(settings?.alias); }, setDataProviders: cssDataManager.setDataProviders.bind(cssDataManager), doValidation: validation.doValidation.bind(validation), diff --git a/src/cssLanguageTypes.ts b/src/cssLanguageTypes.ts index 5c2933a5..d2f0f3f0 100644 --- a/src/cssLanguageTypes.ts +++ b/src/cssLanguageTypes.ts @@ -47,8 +47,14 @@ export interface LanguageSettings { lint?: LintSettings; completion?: CompletionSettings; hover?: HoverSettings; + alias?: AliasSettings; } +export interface AliasSettings { + paths?: { [key: string]: string }; + configPath?: boolean; + baseUrl?: string; +} export interface HoverSettings { documentation?: boolean; diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index bceb7fb8..3c9d491f 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -6,6 +6,7 @@ import * as l10n from '@vscode/l10n'; import { + AliasSettings, Color, ColorInformation, ColorPresentation, @@ -45,19 +46,21 @@ const startsWithSchemeRegex = /^\w+:\/\//; const startsWithData = /^data:/; export class CSSNavigation { - constructor( - protected fileSystemProvider: FileSystemProvider | undefined, - private readonly resolveModuleReferences: boolean - ) {} + private defaultSettings?: AliasSettings; - public findDefinition( - document: TextDocument, - position: Position, - stylesheet: nodes.Node - ): Location | null { - const symbols = new Symbols(stylesheet); - const offset = document.offsetAt(position); - const node = nodes.getNodeAtOffset(stylesheet, offset); + constructor( + protected fileSystemProvider: FileSystemProvider | undefined, + private readonly resolveModuleReferences: boolean, + ) {} + + public configure(settings: AliasSettings | undefined) { + this.defaultSettings = settings; + } + + public findDefinition(document: TextDocument, position: Position, stylesheet: nodes.Node): Location | null { + const symbols = new Symbols(stylesheet); + const offset = document.offsetAt(position); + const node = nodes.getNodeAtOffset(stylesheet, offset); if (!node) { return null; @@ -513,49 +516,51 @@ export class CSSNavigation { return target; } - protected async resolveReference( - target: string, - documentUri: string, - documentContext: DocumentContext, - isRawLink = false - ): Promise { - // Following [css-loader](https://github.com/webpack-contrib/css-loader#url) - // and [sass-loader's](https://github.com/webpack-contrib/sass-loader#imports) - // convention, if an import path starts with ~ then use node module resolution - // *unless* it starts with "~/" as this refers to the user's home directory. - if (target[0] === '~' && target[1] !== '/' && this.fileSystemProvider) { - target = target.substring(1); - return this.mapReference( - await this.resolveModuleReference(target, documentUri, documentContext), - isRawLink - ); - } - - const ref = await this.mapReference( - documentContext.resolveReference(target, documentUri), - isRawLink - ); - - // Following [less-loader](https://github.com/webpack-contrib/less-loader#imports) - // and [sass-loader's](https://github.com/webpack-contrib/sass-loader#resolving-import-at-rules) - // new resolving import at-rules (~ is deprecated). The loader will first try to resolve @import as a relative path. If it cannot be resolved, - // then the loader will try to resolve @import inside node_modules. - if (this.resolveModuleReferences) { - if (ref && (await this.fileExists(ref))) { - return ref; - } - - const moduleReference = await this.mapReference( - await this.resolveModuleReference(target, documentUri, documentContext), - isRawLink - ); - if (moduleReference) { - return moduleReference; - } - } - // fall back. it might not exists - return ref; - } + protected async resolveReference( + target: string, + documentUri: string, + documentContext: DocumentContext, + isRawLink = false, + settings = this.defaultSettings, + ): Promise { + // TEST: . + if (target.indexOf('#Sass/') !== -1) { + console.log('#SASS Target', settings); + target = './test2.css'; + return await this.mapReference(documentContext.resolveReference(target, documentUri), isRawLink); + } + + // Following [css-loader](https://github.com/webpack-contrib/css-loader#url) + // and [sass-loader's](https://github.com/webpack-contrib/sass-loader#imports) + // convention, if an import path starts with ~ then use node module resolution + // *unless* it starts with "~/" as this refers to the user's home directory. + if (target[0] === '~' && target[1] !== '/' && this.fileSystemProvider) { + target = target.substring(1); + return this.mapReference(await this.resolveModuleReference(target, documentUri, documentContext), isRawLink); + } + + const ref = await this.mapReference(documentContext.resolveReference(target, documentUri), isRawLink); + + // Following [less-loader](https://github.com/webpack-contrib/less-loader#imports) + // and [sass-loader's](https://github.com/webpack-contrib/sass-loader#resolving-import-at-rules) + // new resolving import at-rules (~ is deprecated). The loader will first try to resolve @import as a relative path. If it cannot be resolved, + // then the loader will try to resolve @import inside node_modules. + if (this.resolveModuleReferences) { + if (ref && (await this.fileExists(ref))) { + return ref; + } + + const moduleReference = await this.mapReference( + await this.resolveModuleReference(target, documentUri, documentContext), + isRawLink + ); + if (moduleReference) { + return moduleReference; + } + } + // fall back. it might not exists + return ref; + } private async resolvePathToModule( _moduleName: string, From c7f86de6d56e3b6dfb01bb2253dbe94363addacc Mon Sep 17 00:00:00 2001 From: Simon Stranks Date: Sat, 28 Oct 2023 17:23:44 +0100 Subject: [PATCH 03/14] fix(css alias): wip --- src/services/cssNavigation.ts | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index 3c9d491f..2f929c0b 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -523,13 +523,6 @@ export class CSSNavigation { isRawLink = false, settings = this.defaultSettings, ): Promise { - // TEST: . - if (target.indexOf('#Sass/') !== -1) { - console.log('#SASS Target', settings); - target = './test2.css'; - return await this.mapReference(documentContext.resolveReference(target, documentUri), isRawLink); - } - // Following [css-loader](https://github.com/webpack-contrib/css-loader#url) // and [sass-loader's](https://github.com/webpack-contrib/sass-loader#imports) // convention, if an import path starts with ~ then use node module resolution @@ -558,6 +551,33 @@ export class CSSNavigation { return moduleReference; } } + + // TEST: + // Try resolving the reference utilizing aliases defined in settings.json; relative to root working folder + if (ref && !(await this.fileExists(ref))){ + const rootFolderUri = documentContext.resolveReference('/', documentUri); + if (settings?.paths && rootFolderUri) { + for (const [alias, path] of Object.entries(settings.paths)) { + // Reference folder + if (alias[-2] === '/' && alias[-1] === '*'){ + if (startsWith(target, alias)){ + // strip /* from alias-path end + // STR1: join rootFolderURi + above string + // STR2: strip alias (xxx/) from target + // join STR1 + STR2, and return + } + // Do work here + } + // Specific file reference + if (target === alias){ + return joinPath(rootFolderUri, path); + } + } + } + + } + // TEST: . + // fall back. it might not exists return ref; } From fbb5593b2b4c51c5665c80fafcf781ac7e3a396e Mon Sep 17 00:00:00 2001 From: Simon Stranks Date: Sun, 29 Oct 2023 20:36:26 +0000 Subject: [PATCH 04/14] fix(css alias): wip --- src/services/cssNavigation.ts | 36 ++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index 2f929c0b..0e61e1e5 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -30,7 +30,7 @@ import { getColorValue, hslFromColor, hwbFromColor } from '../languageFacts/fact import * as nodes from '../parser/cssNodes'; import { Symbols } from '../parser/cssSymbolScope'; import { dirname, joinPath } from '../utils/resources'; -import { startsWith } from '../utils/strings'; +import { endsWith, startsWith } from '../utils/strings'; type UnresolvedLinkData = { link: DocumentLink; isRawLink: boolean }; @@ -557,20 +557,30 @@ export class CSSNavigation { if (ref && !(await this.fileExists(ref))){ const rootFolderUri = documentContext.resolveReference('/', documentUri); if (settings?.paths && rootFolderUri) { - for (const [alias, path] of Object.entries(settings.paths)) { - // Reference folder - if (alias[-2] === '/' && alias[-1] === '*'){ - if (startsWith(target, alias)){ - // strip /* from alias-path end - // STR1: join rootFolderURi + above string - // STR2: strip alias (xxx/) from target - // join STR1 + STR2, and return - } - // Do work here - } + aliasMap: for (const [alias, aliasPath] of Object.entries(settings.paths)) { + // Skip erroneous user syntax + if (alias === '' || aliasPath === '' || alias[0] === '/' || alias[-1] === '/') {continue aliasMap;} // Specific file reference if (target === alias){ - return joinPath(rootFolderUri, path); + return joinPath(rootFolderUri, aliasPath); + } + // Reference folder + if (endsWith(alias, '/*') && endsWith(aliasPath, '/*')){ + if (startsWith(target, alias.slice(0, -1))){ + let newPath = aliasPath.slice(0, -1); + newPath = joinPath(rootFolderUri, newPath); + const newTarget = target.slice(alias.length - 1); + return newPath = joinPath(newPath, newTarget); + + // NOTE: Works. + // // strip '.' prefix and '*' suffix; concatenate to root folder path + // let newPath = aliasPath.slice(1, -1); + // newPath = `${rootFolderUri}${newPath}`; + // // Strip alias prefix from target and concatenate + // const newTarget = target.slice(alias.length - 2); + // newPath = `${newPath}${newTarget}`; + // return newPath; + } } } } From 7c501828ced8f83650f3ad1f730bdcc44d3b9202 Mon Sep 17 00:00:00 2001 From: Simon Stranks Date: Mon, 30 Oct 2023 16:22:44 +0000 Subject: [PATCH 05/14] fix(css alias): wip --- src/services/cssNavigation.ts | 39 ++++++++++------------------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index 0e61e1e5..88c63b72 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -30,7 +30,7 @@ import { getColorValue, hslFromColor, hwbFromColor } from '../languageFacts/fact import * as nodes from '../parser/cssNodes'; import { Symbols } from '../parser/cssSymbolScope'; import { dirname, joinPath } from '../utils/resources'; -import { endsWith, startsWith } from '../utils/strings'; +import { startsWith } from '../utils/strings'; type UnresolvedLinkData = { link: DocumentLink; isRawLink: boolean }; @@ -46,7 +46,7 @@ const startsWithSchemeRegex = /^\w+:\/\//; const startsWithData = /^data:/; export class CSSNavigation { - private defaultSettings?: AliasSettings; + protected defaultSettings?: AliasSettings; constructor( protected fileSystemProvider: FileSystemProvider | undefined, @@ -552,41 +552,24 @@ export class CSSNavigation { } } - // TEST: - // Try resolving the reference utilizing aliases defined in settings.json; relative to root working folder + // Try resolving the reference utilizing aliases defined in settings.json if (ref && !(await this.fileExists(ref))){ const rootFolderUri = documentContext.resolveReference('/', documentUri); if (settings?.paths && rootFolderUri) { - aliasMap: for (const [alias, aliasPath] of Object.entries(settings.paths)) { - // Skip erroneous user syntax - if (alias === '' || aliasPath === '' || alias[0] === '/' || alias[-1] === '/') {continue aliasMap;} // Specific file reference - if (target === alias){ - return joinPath(rootFolderUri, aliasPath); + if (target in settings.paths){ + return this.mapReference(joinPath(rootFolderUri, settings.paths[target]), isRawLink); } // Reference folder - if (endsWith(alias, '/*') && endsWith(aliasPath, '/*')){ - if (startsWith(target, alias.slice(0, -1))){ - let newPath = aliasPath.slice(0, -1); - newPath = joinPath(rootFolderUri, newPath); - const newTarget = target.slice(alias.length - 1); - return newPath = joinPath(newPath, newTarget); - - // NOTE: Works. - // // strip '.' prefix and '*' suffix; concatenate to root folder path - // let newPath = aliasPath.slice(1, -1); - // newPath = `${rootFolderUri}${newPath}`; - // // Strip alias prefix from target and concatenate - // const newTarget = target.slice(alias.length - 2); - // newPath = `${newPath}${newTarget}`; - // return newPath; - } + const firstSlash = target.indexOf('/'); + const prefix = `${target.substring(0, firstSlash)}/*`; + if (prefix in settings.paths){ + const aliasPath = (settings.paths[prefix]).slice(0, -1); + let newPath = joinPath(rootFolderUri, aliasPath); + return this.mapReference(newPath = joinPath(newPath, target.substring(prefix.length - 1)), isRawLink); } - } } - } - // TEST: . // fall back. it might not exists return ref; From b7759093c6bd51a689d7f6b9507e351f33c1675a Mon Sep 17 00:00:00 2001 From: Simon Stranks Date: Mon, 30 Oct 2023 18:19:42 +0000 Subject: [PATCH 06/14] fix(css alias): implementation complete --- src/services/cssNavigation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index 88c63b72..61c05664 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -552,7 +552,7 @@ export class CSSNavigation { } } - // Try resolving the reference utilizing aliases defined in settings.json + // Try resolving the reference from the language configuration alias settings. if (ref && !(await this.fileExists(ref))){ const rootFolderUri = documentContext.resolveReference('/', documentUri); if (settings?.paths && rootFolderUri) { From 52c5458e5dc6d07229aee14442e3557b4716a0d2 Mon Sep 17 00:00:00 2001 From: Simon Stranks Date: Mon, 30 Oct 2023 18:52:27 +0000 Subject: [PATCH 07/14] fix() --- src/services/cssNavigation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index 61c05664..ae5d2a80 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -552,7 +552,7 @@ export class CSSNavigation { } } - // Try resolving the reference from the language configuration alias settings. + // Try resolving the reference from the language configuration alias settings if (ref && !(await this.fileExists(ref))){ const rootFolderUri = documentContext.resolveReference('/', documentUri); if (settings?.paths && rootFolderUri) { From cb2ed4ac131fd7fb758974fdb829ac021dd746af Mon Sep 17 00:00:00 2001 From: Simon Stranks Date: Mon, 30 Oct 2023 18:57:46 +0000 Subject: [PATCH 08/14] fix(alias type): finalized --- src/cssLanguageTypes.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cssLanguageTypes.ts b/src/cssLanguageTypes.ts index d2f0f3f0..9c6f95fd 100644 --- a/src/cssLanguageTypes.ts +++ b/src/cssLanguageTypes.ts @@ -52,8 +52,6 @@ export interface LanguageSettings { export interface AliasSettings { paths?: { [key: string]: string }; - configPath?: boolean; - baseUrl?: string; } export interface HoverSettings { From 04730bb0f77b6fcd922933839165e38ad9d79d76 Mon Sep 17 00:00:00 2001 From: Simon Stranks Date: Mon, 30 Oct 2023 19:01:45 +0000 Subject: [PATCH 09/14] fix(): reverting to main files --- .mocharc.js | 9 ------ .mocharc.json | 6 ++++ .vscode/OpenSourceContrib.code-workspace | 35 ------------------------ .vscode/launch.json | 17 ++++++++---- 4 files changed, 18 insertions(+), 49 deletions(-) delete mode 100644 .mocharc.js create mode 100644 .mocharc.json delete mode 100644 .vscode/OpenSourceContrib.code-workspace diff --git a/.mocharc.js b/.mocharc.js deleted file mode 100644 index f99c035d..00000000 --- a/.mocharc.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -module.exports = { - ui: 'tdd', - color: true, - // spec: './lib/umd/test/**/*.test.js', // aLl tests - spec: './lib/umd/test/css/navigation.test.js', // single test - recursive: true, -}; diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 00000000..53a15187 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,6 @@ +{ + "ui": "tdd", + "color": true, + "spec": "./lib/umd/test/**/*.test.js", + "recursive": true +} \ No newline at end of file diff --git a/.vscode/OpenSourceContrib.code-workspace b/.vscode/OpenSourceContrib.code-workspace deleted file mode 100644 index e16b504a..00000000 --- a/.vscode/OpenSourceContrib.code-workspace +++ /dev/null @@ -1,35 +0,0 @@ -{ - "folders": [ - { - "path": "../" - } - ], - - "settings": { - // ESLint looks to for 'import/resolver/' typescript; project. - // "eslint.workingDirectories": ["./frontend", "./backend"], - "workbench.editor.wrapTabs": false, - "workbench.startupEditor": "none", - "task.allowAutomaticTasks": "on" - // "path-intellisense.mappings": { - // "#Img": "${workspaceFolder}/frontend/src/assets/img", - // "#Sass": "${workspaceFolder}/frontend/src/assets/sass", - // "#Svg": "${workspaceFolder}/frontend/src/assets/svg", - // "#Components": "${workspaceFolder}/frontend/src/components", - // "#Context": "${workspaceFolder}/frontend/src/context", - // "#Data": "${workspaceFolder}/frontend/src/data", - // "#Feature": "${workspaceFolder}/frontend/src/features", - // "#Hooks": "${workspaceFolder}/frontend/src/hooks", - // "#Layouts": "${workspaceFolder}/frontend/src/layouts", - // "#Lib": "${workspaceFolder}/frontend/src/lib", - // "#Pages": "${workspaceFolder}/frontend/src/pages", - // "#Services": "${workspaceFolder}/frontend/src/services", - // "#Types": "${workspaceFolder}/frontend/src/types", - // "#Utils": "${workspaceFolder}/frontend/src/utils" - // } - }, - "launch": { - "version": "0.2.0", - "configurations": [] - } -} diff --git a/.vscode/launch.json b/.vscode/launch.json index 81d5f99e..a45fb872 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,20 +4,27 @@ { "name": "Unit Tests", "type": "node", - "runtimeVersion": "18.16.0", "request": "launch", "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", "stopOnEntry": false, - "args": ["--timeout", "999999", "--colors"], // in .mocharc.js + "args": [ + "--timeout", + "999999", + "--colors" + ], "cwd": "${workspaceRoot}", "runtimeExecutable": null, "runtimeArgs": [], "env": {}, "sourceMaps": true, - "outFiles": ["${workspaceRoot}/lib/umd/**"], - "skipFiles": ["/**"], + "outFiles": [ + "${workspaceRoot}/lib/umd/**" + ], + "skipFiles": [ + "/**" + ], "smartStep": true, "preLaunchTask": "npm: watch" } ] -} +} \ No newline at end of file From c581676ec59173c073b8404e6469616edbf5422c Mon Sep 17 00:00:00 2001 From: Simon Stranks Date: Mon, 30 Oct 2023 19:23:21 +0000 Subject: [PATCH 10/14] fix(formatting): --- src/services/cssNavigation.ts | 1032 ++++++++++++++------------------- 1 file changed, 441 insertions(+), 591 deletions(-) diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index ae5d2a80..7ef401c5 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -4,525 +4,385 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as l10n from '@vscode/l10n'; import { - AliasSettings, - Color, - ColorInformation, - ColorPresentation, - DocumentContext, - DocumentHighlight, - DocumentHighlightKind, - DocumentLink, - DocumentSymbol, - FileSystemProvider, - FileType, - Location, - Position, - Range, - SymbolInformation, - SymbolKind, - TextDocument, - TextEdit, - WorkspaceEdit, + AliasSettings, Color, ColorInformation, ColorPresentation, DocumentHighlight, DocumentHighlightKind, DocumentLink, Location, + Position, Range, SymbolInformation, SymbolKind, TextEdit, WorkspaceEdit, TextDocument, DocumentContext, FileSystemProvider, FileType, DocumentSymbol } from '../cssLanguageTypes'; -import { getColorValue, hslFromColor, hwbFromColor } from '../languageFacts/facts'; +import * as l10n from '@vscode/l10n'; import * as nodes from '../parser/cssNodes'; import { Symbols } from '../parser/cssSymbolScope'; -import { dirname, joinPath } from '../utils/resources'; +import { getColorValue, hslFromColor, hwbFromColor } from '../languageFacts/facts'; import { startsWith } from '../utils/strings'; +import { dirname, joinPath } from '../utils/resources'; + -type UnresolvedLinkData = { link: DocumentLink; isRawLink: boolean }; +type UnresolvedLinkData = { link: DocumentLink, isRawLink: boolean }; -type DocumentSymbolCollector = ( - name: string, - kind: SymbolKind, - symbolNodeOrRange: nodes.Node | Range, - nameNodeOrRange: nodes.Node | Range | undefined, - bodyNode: nodes.Node | undefined -) => void; +type DocumentSymbolCollector = (name: string, kind: SymbolKind, symbolNodeOrRange: nodes.Node | Range, nameNodeOrRange: nodes.Node | Range | undefined, bodyNode: nodes.Node | undefined) => void; const startsWithSchemeRegex = /^\w+:\/\//; const startsWithData = /^data:/; export class CSSNavigation { - protected defaultSettings?: AliasSettings; + protected defaultSettings?: AliasSettings; - constructor( - protected fileSystemProvider: FileSystemProvider | undefined, - private readonly resolveModuleReferences: boolean, - ) {} + constructor(protected fileSystemProvider: FileSystemProvider | undefined, private readonly resolveModuleReferences: boolean) { + } - public configure(settings: AliasSettings | undefined) { + public configure(settings: AliasSettings | undefined) { this.defaultSettings = settings; } public findDefinition(document: TextDocument, position: Position, stylesheet: nodes.Node): Location | null { + const symbols = new Symbols(stylesheet); const offset = document.offsetAt(position); const node = nodes.getNodeAtOffset(stylesheet, offset); - if (!node) { - return null; - } + if (!node) { + return null; + } - const symbol = symbols.findSymbolFromNode(node); - if (!symbol) { - return null; - } + const symbol = symbols.findSymbolFromNode(node); + if (!symbol) { + return null; + } - return { - uri: document.uri, - range: getRange(symbol.node, document), - }; - } - - public findReferences( - document: TextDocument, - position: Position, - stylesheet: nodes.Stylesheet - ): Location[] { - const highlights = this.findDocumentHighlights(document, position, stylesheet); - return highlights.map((h) => { - return { - uri: document.uri, - range: h.range, - }; - }); - } - - private getHighlightNode( - document: TextDocument, - position: Position, - stylesheet: nodes.Stylesheet - ): nodes.Node | undefined { - const offset = document.offsetAt(position); - let node = nodes.getNodeAtOffset(stylesheet, offset); - if ( - !node || - node.type === nodes.NodeType.Stylesheet || - node.type === nodes.NodeType.Declarations - ) { - return; - } - if ( - node.type === nodes.NodeType.Identifier && - node.parent && - node.parent.type === nodes.NodeType.ClassSelector - ) { - node = node.parent; - } + return { + uri: document.uri, + range: getRange(symbol.node, document) + }; + } - return node; - } - - public findDocumentHighlights( - document: TextDocument, - position: Position, - stylesheet: nodes.Stylesheet - ): DocumentHighlight[] { - const result: DocumentHighlight[] = []; - const node = this.getHighlightNode(document, position, stylesheet); - if (!node) { - return result; - } + public findReferences(document: TextDocument, position: Position, stylesheet: nodes.Stylesheet): Location[] { + const highlights = this.findDocumentHighlights(document, position, stylesheet); + return highlights.map(h => { + return { + uri: document.uri, + range: h.range + }; + }); + } - const symbols = new Symbols(stylesheet); - const symbol = symbols.findSymbolFromNode(node); - const name = node.getText(); - - stylesheet.accept((candidate) => { - if (symbol) { - if (symbols.matchesSymbol(candidate, symbol)) { - result.push({ - kind: getHighlightKind(candidate), - range: getRange(candidate, document), - }); - return false; - } - } else if (node && node.type === candidate.type && candidate.matches(name)) { - // Same node type and data - result.push({ - kind: getHighlightKind(candidate), - range: getRange(candidate, document), - }); - } - return true; - }); - - return result; - } - - protected isRawStringDocumentLinkNode(node: nodes.Node): boolean { - return node.type === nodes.NodeType.Import; - } - - public findDocumentLinks( - document: TextDocument, - stylesheet: nodes.Stylesheet, - documentContext: DocumentContext - ): DocumentLink[] { - const linkData = this.findUnresolvedLinks(document, stylesheet); - const resolvedLinks: DocumentLink[] = []; - for (let data of linkData) { - const link = data.link; - const target = link.target; - if (!target || startsWithData.test(target)) { - // no links for data: - } else if (startsWithSchemeRegex.test(target)) { - resolvedLinks.push(link); - } else { - const resolved = documentContext.resolveReference(target, document.uri); - if (resolved) { - link.target = resolved; - } - resolvedLinks.push(link); - } - } - return resolvedLinks; - } - - public async findDocumentLinks2( - document: TextDocument, - stylesheet: nodes.Stylesheet, - documentContext: DocumentContext - ): Promise { - const linkData = this.findUnresolvedLinks(document, stylesheet); - const resolvedLinks: DocumentLink[] = []; - for (let data of linkData) { - const link = data.link; - const target = link.target; - if (!target || startsWithData.test(target)) { - // no links for data: - } else if (startsWithSchemeRegex.test(target)) { - resolvedLinks.push(link); - } else { - const resolvedTarget = await this.resolveReference( - target, - document.uri, - documentContext, - data.isRawLink - ); - if (resolvedTarget !== undefined) { - link.target = resolvedTarget; - resolvedLinks.push(link); - } - } - } - return resolvedLinks; - } - - private findUnresolvedLinks( - document: TextDocument, - stylesheet: nodes.Stylesheet - ): UnresolvedLinkData[] { - const result: UnresolvedLinkData[] = []; - - const collect = (uriStringNode: nodes.Node) => { - let rawUri = uriStringNode.getText(); - const range = getRange(uriStringNode, document); - // Make sure the range is not empty - if (range.start.line === range.end.line && range.start.character === range.end.character) { - return; - } + private getHighlightNode(document: TextDocument, position: Position, stylesheet: nodes.Stylesheet): nodes.Node | undefined { + const offset = document.offsetAt(position); + let node = nodes.getNodeAtOffset(stylesheet, offset); + if (!node || node.type === nodes.NodeType.Stylesheet || node.type === nodes.NodeType.Declarations) { + return; + } + if (node.type === nodes.NodeType.Identifier && node.parent && node.parent.type === nodes.NodeType.ClassSelector) { + node = node.parent; + } - if (startsWith(rawUri, `'`) || startsWith(rawUri, `"`)) { - rawUri = rawUri.slice(1, -1); - } + return node; + } - const isRawLink = uriStringNode.parent - ? this.isRawStringDocumentLinkNode(uriStringNode.parent) - : false; - result.push({ link: { target: rawUri, range }, isRawLink }); - }; - - stylesheet.accept((candidate) => { - if (candidate.type === nodes.NodeType.URILiteral) { - const first = candidate.getChild(0); - if (first) { - collect(first); - } - return false; - } + public findDocumentHighlights(document: TextDocument, position: Position, stylesheet: nodes.Stylesheet): DocumentHighlight[] { + const result: DocumentHighlight[] = []; + const node = this.getHighlightNode(document, position, stylesheet); + if (!node) { + return result; + } - /** - * In @import, it is possible to include links that do not use `url()` - * For example, `@import 'foo.css';` - */ - if (candidate.parent && this.isRawStringDocumentLinkNode(candidate.parent)) { - const rawText = candidate.getText(); - if (startsWith(rawText, `'`) || startsWith(rawText, `"`)) { - collect(candidate); - } - return false; - } + const symbols = new Symbols(stylesheet); + const symbol = symbols.findSymbolFromNode(node); + const name = node.getText(); + + stylesheet.accept(candidate => { + if (symbol) { + if (symbols.matchesSymbol(candidate, symbol)) { + result.push({ + kind: getHighlightKind(candidate), + range: getRange(candidate, document) + }); + return false; + } + } else if (node && node.type === candidate.type && candidate.matches(name)) { + // Same node type and data + result.push({ + kind: getHighlightKind(candidate), + range: getRange(candidate, document) + }); + } + return true; + }); - return true; - }); - - return result; - } - - public findSymbolInformations( - document: TextDocument, - stylesheet: nodes.Stylesheet - ): SymbolInformation[] { - const result: SymbolInformation[] = []; - - const addSymbolInformation = ( - name: string, - kind: SymbolKind, - symbolNodeOrRange: nodes.Node | Range - ) => { - const range = - symbolNodeOrRange instanceof nodes.Node - ? getRange(symbolNodeOrRange, document) - : symbolNodeOrRange; - const entry: SymbolInformation = { - name: name || l10n.t(''), - kind, - location: Location.create(document.uri, range), - }; - result.push(entry); - }; - - this.collectDocumentSymbols(document, stylesheet, addSymbolInformation); - - return result; - } - - public findDocumentSymbols( - document: TextDocument, - stylesheet: nodes.Stylesheet - ): DocumentSymbol[] { - const result: DocumentSymbol[] = []; - - const parents: [DocumentSymbol, Range][] = []; - - const addDocumentSymbol = ( - name: string, - kind: SymbolKind, - symbolNodeOrRange: nodes.Node | Range, - nameNodeOrRange: nodes.Node | Range | undefined, - bodyNode: nodes.Node | undefined - ) => { - const range = - symbolNodeOrRange instanceof nodes.Node - ? getRange(symbolNodeOrRange, document) - : symbolNodeOrRange; - let selectionRange = - nameNodeOrRange instanceof nodes.Node - ? getRange(nameNodeOrRange, document) - : nameNodeOrRange; - if (!selectionRange || !containsRange(range, selectionRange)) { - selectionRange = Range.create(range.start, range.start); - } + return result; + } - const entry: DocumentSymbol = { - name: name || l10n.t(''), - kind, - range, - selectionRange, - }; - let top = parents.pop(); - while (top && !containsRange(top[1], range)) { - top = parents.pop(); - } - if (top) { - const topSymbol = top[0]; - if (!topSymbol.children) { - topSymbol.children = []; - } - topSymbol.children.push(entry); - parents.push(top); // put back top - } else { - result.push(entry); - } - if (bodyNode) { - parents.push([entry, getRange(bodyNode, document)]); - } - }; - - this.collectDocumentSymbols(document, stylesheet, addDocumentSymbol); - - return result; - } - - private collectDocumentSymbols( - document: TextDocument, - stylesheet: nodes.Stylesheet, - collect: DocumentSymbolCollector - ): void { - stylesheet.accept((node) => { - if (node instanceof nodes.RuleSet) { - for (const selector of node.getSelectors().getChildren()) { - if (selector instanceof nodes.Selector) { - const range = Range.create( - document.positionAt(selector.offset), - document.positionAt(node.end) - ); - collect(selector.getText(), SymbolKind.Class, range, selector, node.getDeclarations()); - } - } - } else if (node instanceof nodes.VariableDeclaration) { - collect(node.getName(), SymbolKind.Variable, node, node.getVariable(), undefined); - } else if (node instanceof nodes.MixinDeclaration) { - collect( - node.getName(), - SymbolKind.Method, - node, - node.getIdentifier(), - node.getDeclarations() - ); - } else if (node instanceof nodes.FunctionDeclaration) { - collect( - node.getName(), - SymbolKind.Function, - node, - node.getIdentifier(), - node.getDeclarations() - ); - } else if (node instanceof nodes.Keyframe) { - const name = l10n.t('@keyframes {0}', node.getName()); - collect(name, SymbolKind.Class, node, node.getIdentifier(), node.getDeclarations()); - } else if (node instanceof nodes.FontFace) { - const name = l10n.t('@font-face'); - collect(name, SymbolKind.Class, node, undefined, node.getDeclarations()); - } else if (node instanceof nodes.Media) { - const mediaList = node.getChild(0); - if (mediaList instanceof nodes.Medialist) { - const name = '@media ' + mediaList.getText(); - collect(name, SymbolKind.Module, node, mediaList, node.getDeclarations()); - } - } - return true; - }); - } - - public findDocumentColors( - document: TextDocument, - stylesheet: nodes.Stylesheet - ): ColorInformation[] { - const result: ColorInformation[] = []; - stylesheet.accept((node) => { - const colorInfo = getColorInformation(node, document); - if (colorInfo) { - result.push(colorInfo); - } - return true; - }); - return result; - } - - public getColorPresentations( - document: TextDocument, - stylesheet: nodes.Stylesheet, - color: Color, - range: Range - ): ColorPresentation[] { - const result: ColorPresentation[] = []; - const red256 = Math.round(color.red * 255), - green256 = Math.round(color.green * 255), - blue256 = Math.round(color.blue * 255); - - let label; - if (color.alpha === 1) { - label = `rgb(${red256}, ${green256}, ${blue256})`; - } else { - label = `rgba(${red256}, ${green256}, ${blue256}, ${color.alpha})`; - } - result.push({ label: label, textEdit: TextEdit.replace(range, label) }); - - if (color.alpha === 1) { - label = `#${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex(blue256)}`; - } else { - label = `#${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex( - blue256 - )}${toTwoDigitHex(Math.round(color.alpha * 255))}`; - } - result.push({ label: label, textEdit: TextEdit.replace(range, label) }); + protected isRawStringDocumentLinkNode(node: nodes.Node): boolean { + return node.type === nodes.NodeType.Import; + } - const hsl = hslFromColor(color); - if (hsl.a === 1) { - label = `hsl(${hsl.h}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%)`; - } else { - label = `hsla(${hsl.h}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%, ${hsl.a})`; - } - result.push({ label: label, textEdit: TextEdit.replace(range, label) }); + public findDocumentLinks(document: TextDocument, stylesheet: nodes.Stylesheet, documentContext: DocumentContext): DocumentLink[] { + const linkData = this.findUnresolvedLinks(document, stylesheet); + const resolvedLinks: DocumentLink[] = []; + for (let data of linkData) { + const link = data.link; + const target = link.target; + if (!target || startsWithData.test(target)) { + // no links for data: + } else if (startsWithSchemeRegex.test(target)) { + resolvedLinks.push(link); + } else { + const resolved = documentContext.resolveReference(target, document.uri); + if (resolved) { + link.target = resolved; + } + resolvedLinks.push(link); + } + } + return resolvedLinks; + } + + public async findDocumentLinks2(document: TextDocument, stylesheet: nodes.Stylesheet, documentContext: DocumentContext): Promise { + const linkData = this.findUnresolvedLinks(document, stylesheet); + const resolvedLinks: DocumentLink[] = []; + for (let data of linkData) { + const link = data.link; + const target = link.target; + if (!target || startsWithData.test(target)) { + // no links for data: + } else if (startsWithSchemeRegex.test(target)) { + resolvedLinks.push(link); + } else { + const resolvedTarget = await this.resolveReference(target, document.uri, documentContext, data.isRawLink); + if (resolvedTarget !== undefined) { + link.target = resolvedTarget; + resolvedLinks.push(link); + } + } + } + return resolvedLinks; + } + + + private findUnresolvedLinks(document: TextDocument, stylesheet: nodes.Stylesheet): UnresolvedLinkData[] { + const result: UnresolvedLinkData[] = []; + + const collect = (uriStringNode: nodes.Node) => { + let rawUri = uriStringNode.getText(); + const range = getRange(uriStringNode, document); + // Make sure the range is not empty + if (range.start.line === range.end.line && range.start.character === range.end.character) { + return; + } + + if (startsWith(rawUri, `'`) || startsWith(rawUri, `"`)) { + rawUri = rawUri.slice(1, -1); + } + + const isRawLink = uriStringNode.parent ? this.isRawStringDocumentLinkNode(uriStringNode.parent) : false; + result.push({ link: { target: rawUri, range }, isRawLink }); + }; + + stylesheet.accept(candidate => { + if (candidate.type === nodes.NodeType.URILiteral) { + const first = candidate.getChild(0); + if (first) { + collect(first); + } + return false; + } + + /** + * In @import, it is possible to include links that do not use `url()` + * For example, `@import 'foo.css';` + */ + if (candidate.parent && this.isRawStringDocumentLinkNode(candidate.parent)) { + const rawText = candidate.getText(); + if (startsWith(rawText, `'`) || startsWith(rawText, `"`)) { + collect(candidate); + } + return false; + } + + return true; + }); + + return result; + } + + public findSymbolInformations(document: TextDocument, stylesheet: nodes.Stylesheet): SymbolInformation[] { + + const result: SymbolInformation[] = []; + + const addSymbolInformation = (name: string, kind: SymbolKind, symbolNodeOrRange: nodes.Node | Range) => { + const range = symbolNodeOrRange instanceof nodes.Node ? getRange(symbolNodeOrRange, document) : symbolNodeOrRange; + const entry: SymbolInformation = { + name: name || l10n.t(''), + kind, + location: Location.create(document.uri, range) + }; + result.push(entry); + }; + + this.collectDocumentSymbols(document, stylesheet, addSymbolInformation); + + return result; + } + + public findDocumentSymbols(document: TextDocument, stylesheet: nodes.Stylesheet): DocumentSymbol[] { + const result: DocumentSymbol[] = []; + + const parents: [DocumentSymbol, Range][] = []; + + const addDocumentSymbol = (name: string, kind: SymbolKind, symbolNodeOrRange: nodes.Node | Range, nameNodeOrRange: nodes.Node | Range | undefined, bodyNode: nodes.Node | undefined) => { + const range = symbolNodeOrRange instanceof nodes.Node ? getRange(symbolNodeOrRange, document) : symbolNodeOrRange; + let selectionRange = nameNodeOrRange instanceof nodes.Node ? getRange(nameNodeOrRange, document) : nameNodeOrRange; + if (!selectionRange || !containsRange(range, selectionRange)) { + selectionRange = Range.create(range.start, range.start); + } + + const entry: DocumentSymbol = { + name: name || l10n.t(''), + kind, + range, + selectionRange + }; + let top = parents.pop(); + while (top && !containsRange(top[1], range)) { + top = parents.pop(); + } + if (top) { + const topSymbol = top[0]; + if (!topSymbol.children) { + topSymbol.children = []; + } + topSymbol.children.push(entry); + parents.push(top); // put back top + } else { + result.push(entry); + } + if (bodyNode) { + parents.push([entry, getRange(bodyNode, document)]); + } + }; + + this.collectDocumentSymbols(document, stylesheet, addDocumentSymbol); + + return result; + } + + private collectDocumentSymbols(document: TextDocument, stylesheet: nodes.Stylesheet, collect: DocumentSymbolCollector): void { + stylesheet.accept(node => { + if (node instanceof nodes.RuleSet) { + for (const selector of node.getSelectors().getChildren()) { + if (selector instanceof nodes.Selector) { + const range = Range.create(document.positionAt(selector.offset), document.positionAt(node.end)); + collect(selector.getText(), SymbolKind.Class, range, selector, node.getDeclarations()); + } + } + } else if (node instanceof nodes.VariableDeclaration) { + collect(node.getName(), SymbolKind.Variable, node, node.getVariable(), undefined); + } else if (node instanceof nodes.MixinDeclaration) { + collect(node.getName(), SymbolKind.Method, node, node.getIdentifier(), node.getDeclarations()); + } else if (node instanceof nodes.FunctionDeclaration) { + collect(node.getName(), SymbolKind.Function, node, node.getIdentifier(), node.getDeclarations()); + } else if (node instanceof nodes.Keyframe) { + const name = l10n.t("@keyframes {0}", node.getName()); + collect(name, SymbolKind.Class, node, node.getIdentifier(), node.getDeclarations()); + } else if (node instanceof nodes.FontFace) { + const name = l10n.t("@font-face"); + collect(name, SymbolKind.Class, node, undefined, node.getDeclarations()); + } else if (node instanceof nodes.Media) { + const mediaList = node.getChild(0); + if (mediaList instanceof nodes.Medialist) { + const name = '@media ' + mediaList.getText(); + collect(name, SymbolKind.Module, node, mediaList, node.getDeclarations()); + } + } + return true; + }); + } + + public findDocumentColors(document: TextDocument, stylesheet: nodes.Stylesheet): ColorInformation[] { + const result: ColorInformation[] = []; + stylesheet.accept((node) => { + const colorInfo = getColorInformation(node, document); + if (colorInfo) { + result.push(colorInfo); + } + return true; + }); + return result; + } + + public getColorPresentations(document: TextDocument, stylesheet: nodes.Stylesheet, color: Color, range: Range): ColorPresentation[] { + const result: ColorPresentation[] = []; + const red256 = Math.round(color.red * 255), green256 = Math.round(color.green * 255), blue256 = Math.round(color.blue * 255); + + let label; + if (color.alpha === 1) { + label = `rgb(${red256}, ${green256}, ${blue256})`; + } else { + label = `rgba(${red256}, ${green256}, ${blue256}, ${color.alpha})`; + } + result.push({ label: label, textEdit: TextEdit.replace(range, label) }); + + if (color.alpha === 1) { + label = `#${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex(blue256)}`; + } else { + label = `#${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex(blue256)}${toTwoDigitHex(Math.round(color.alpha * 255))}`; + } + result.push({ label: label, textEdit: TextEdit.replace(range, label) }); + + const hsl = hslFromColor(color); + if (hsl.a === 1) { + label = `hsl(${hsl.h}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%)`; + } else { + label = `hsla(${hsl.h}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%, ${hsl.a})`; + } + result.push({ label: label, textEdit: TextEdit.replace(range, label) }); + + const hwb = hwbFromColor(color); + if (hwb.a === 1) { + label = `hwb(${hwb.h} ${Math.round(hwb.w * 100)}% ${Math.round(hwb.b * 100)}%)`; + } else { + label = `hwb(${hwb.h} ${Math.round(hwb.w * 100)}% ${Math.round(hwb.b * 100)}% / ${hwb.a})`; + } + result.push({ label: label, textEdit: TextEdit.replace(range, label) }); + + return result; + } + + public prepareRename(document: TextDocument, position: Position, stylesheet: nodes.Stylesheet): Range | undefined { + const node = this.getHighlightNode(document, position, stylesheet); + if (node) { + return Range.create(document.positionAt(node.offset), document.positionAt(node.end)); + } + } + + public doRename(document: TextDocument, position: Position, newName: string, stylesheet: nodes.Stylesheet): WorkspaceEdit { + const highlights = this.findDocumentHighlights(document, position, stylesheet); + const edits = highlights.map(h => TextEdit.replace(h.range, newName)); + return { + changes: { [document.uri]: edits } + }; + } + + protected async resolveModuleReference(ref: string, documentUri: string, documentContext: DocumentContext): Promise { + if (startsWith(documentUri, 'file://')) { + const moduleName = getModuleNameFromPath(ref); + if (moduleName && moduleName !== '.' && moduleName !== '..') { + const rootFolderUri = documentContext.resolveReference('/', documentUri); + const documentFolderUri = dirname(documentUri); + const modulePath = await this.resolvePathToModule(moduleName, documentFolderUri, rootFolderUri); + if (modulePath) { + const pathWithinModule = ref.substring(moduleName.length + 1); + return joinPath(modulePath, pathWithinModule); + } + } + } + return undefined; + } + + protected async mapReference(target: string | undefined, isRawLink: boolean): Promise { + return target; + } + + protected async resolveReference(target: string, documentUri: string, documentContext: DocumentContext, isRawLink = false, settings = this.defaultSettings): Promise { - const hwb = hwbFromColor(color); - if (hwb.a === 1) { - label = `hwb(${hwb.h} ${Math.round(hwb.w * 100)}% ${Math.round(hwb.b * 100)}%)`; - } else { - label = `hwb(${hwb.h} ${Math.round(hwb.w * 100)}% ${Math.round(hwb.b * 100)}% / ${hwb.a})`; - } - result.push({ label: label, textEdit: TextEdit.replace(range, label) }); - - return result; - } - - public prepareRename( - document: TextDocument, - position: Position, - stylesheet: nodes.Stylesheet - ): Range | undefined { - const node = this.getHighlightNode(document, position, stylesheet); - if (node) { - return Range.create(document.positionAt(node.offset), document.positionAt(node.end)); - } - } - - public doRename( - document: TextDocument, - position: Position, - newName: string, - stylesheet: nodes.Stylesheet - ): WorkspaceEdit { - const highlights = this.findDocumentHighlights(document, position, stylesheet); - const edits = highlights.map((h) => TextEdit.replace(h.range, newName)); - return { - changes: { [document.uri]: edits }, - }; - } - - protected async resolveModuleReference( - ref: string, - documentUri: string, - documentContext: DocumentContext - ): Promise { - if (startsWith(documentUri, 'file://')) { - const moduleName = getModuleNameFromPath(ref); - if (moduleName && moduleName !== '.' && moduleName !== '..') { - const rootFolderUri = documentContext.resolveReference('/', documentUri); - const documentFolderUri = dirname(documentUri); - const modulePath = await this.resolvePathToModule( - moduleName, - documentFolderUri, - rootFolderUri - ); - if (modulePath) { - const pathWithinModule = ref.substring(moduleName.length + 1); - return joinPath(modulePath, pathWithinModule); - } - } - } - return undefined; - } - - protected async mapReference( - target: string | undefined, - isRawLink: boolean - ): Promise { - return target; - } - - protected async resolveReference( - target: string, - documentUri: string, - documentContext: DocumentContext, - isRawLink = false, - settings = this.defaultSettings, - ): Promise { // Following [css-loader](https://github.com/webpack-contrib/css-loader#url) // and [sass-loader's](https://github.com/webpack-contrib/sass-loader#imports) // convention, if an import path starts with ~ then use node module resolution @@ -539,20 +399,17 @@ export class CSSNavigation { // new resolving import at-rules (~ is deprecated). The loader will first try to resolve @import as a relative path. If it cannot be resolved, // then the loader will try to resolve @import inside node_modules. if (this.resolveModuleReferences) { - if (ref && (await this.fileExists(ref))) { + if (ref && await this.fileExists(ref)) { return ref; } - const moduleReference = await this.mapReference( - await this.resolveModuleReference(target, documentUri, documentContext), - isRawLink - ); + const moduleReference = await this.mapReference(await this.resolveModuleReference(target, documentUri, documentContext), isRawLink); if (moduleReference) { return moduleReference; } } - // Try resolving the reference from the language configuration alias settings + // Try resolving the reference from the language configuration alias settings if (ref && !(await this.fileExists(ref))){ const rootFolderUri = documentContext.resolveReference('/', documentUri); if (settings?.paths && rootFolderUri) { @@ -575,126 +432,119 @@ export class CSSNavigation { return ref; } - private async resolvePathToModule( - _moduleName: string, - documentFolderUri: string, - rootFolderUri: string | undefined - ): Promise { - // resolve the module relative to the document. We can't use `require` here as the code is webpacked. - - const packPath = joinPath(documentFolderUri, 'node_modules', _moduleName, 'package.json'); - if (await this.fileExists(packPath)) { - return dirname(packPath); - } else if ( - rootFolderUri && - documentFolderUri.startsWith(rootFolderUri) && - documentFolderUri.length !== rootFolderUri.length - ) { - return this.resolvePathToModule(_moduleName, dirname(documentFolderUri), rootFolderUri); - } - return undefined; - } + private async resolvePathToModule(_moduleName: string, documentFolderUri: string, rootFolderUri: string | undefined): Promise { + // resolve the module relative to the document. We can't use `require` here as the code is webpacked. - protected async fileExists(uri: string): Promise { - if (!this.fileSystemProvider) { - return false; - } - try { - const stat = await this.fileSystemProvider.stat(uri); - if (stat.type === FileType.Unknown && stat.size === -1) { - return false; - } + const packPath = joinPath(documentFolderUri, 'node_modules', _moduleName, 'package.json'); + if (await this.fileExists(packPath)) { + return dirname(packPath); + } else if (rootFolderUri && documentFolderUri.startsWith(rootFolderUri) && (documentFolderUri.length !== rootFolderUri.length)) { + return this.resolvePathToModule(_moduleName, dirname(documentFolderUri), rootFolderUri); + } + return undefined; + } + + protected async fileExists(uri: string): Promise { + if (!this.fileSystemProvider) { + return false; + } + try { + const stat = await this.fileSystemProvider.stat(uri); + if (stat.type === FileType.Unknown && stat.size === -1) { + return false; + } + + return true; + } catch (err) { + return false; + } + } - return true; - } catch (err) { - return false; - } - } } function getColorInformation(node: nodes.Node, document: TextDocument): ColorInformation | null { - const color = getColorValue(node); - if (color) { - const range = getRange(node, document); - return { color, range }; - } - return null; + const color = getColorValue(node); + if (color) { + const range = getRange(node, document); + return { color, range }; + } + return null; } + function getRange(node: nodes.Node, document: TextDocument): Range { - return Range.create(document.positionAt(node.offset), document.positionAt(node.end)); + return Range.create(document.positionAt(node.offset), document.positionAt(node.end)); } /** * Test if `otherRange` is in `range`. If the ranges are equal, will return true. */ function containsRange(range: Range, otherRange: Range): boolean { - const otherStartLine = otherRange.start.line, - otherEndLine = otherRange.end.line; - const rangeStartLine = range.start.line, - rangeEndLine = range.end.line; - - if (otherStartLine < rangeStartLine || otherEndLine < rangeStartLine) { - return false; - } - if (otherStartLine > rangeEndLine || otherEndLine > rangeEndLine) { - return false; - } - if (otherStartLine === rangeStartLine && otherRange.start.character < range.start.character) { - return false; - } - if (otherEndLine === rangeEndLine && otherRange.end.character > range.end.character) { - return false; - } - return true; + const otherStartLine = otherRange.start.line, otherEndLine = otherRange.end.line; + const rangeStartLine = range.start.line, rangeEndLine = range.end.line; + + if (otherStartLine < rangeStartLine || otherEndLine < rangeStartLine) { + return false; + } + if (otherStartLine > rangeEndLine || otherEndLine > rangeEndLine) { + return false; + } + if (otherStartLine === rangeStartLine && otherRange.start.character < range.start.character) { + return false; + } + if (otherEndLine === rangeEndLine && otherRange.end.character > range.end.character) { + return false; + } + return true; } function getHighlightKind(node: nodes.Node): DocumentHighlightKind { - if (node.type === nodes.NodeType.Selector) { - return DocumentHighlightKind.Write; - } - - if (node instanceof nodes.Identifier) { - if (node.parent && node.parent instanceof nodes.Property) { - if (node.isCustomProperty) { - return DocumentHighlightKind.Write; - } - } - } - - if (node.parent) { - switch (node.parent.type) { - case nodes.NodeType.FunctionDeclaration: - case nodes.NodeType.MixinDeclaration: - case nodes.NodeType.Keyframe: - case nodes.NodeType.VariableDeclaration: - case nodes.NodeType.FunctionParameter: - return DocumentHighlightKind.Write; - } - } - return DocumentHighlightKind.Read; + if (node.type === nodes.NodeType.Selector) { + return DocumentHighlightKind.Write; + } + + if (node instanceof nodes.Identifier) { + if (node.parent && node.parent instanceof nodes.Property) { + if (node.isCustomProperty) { + return DocumentHighlightKind.Write; + } + } + } + + if (node.parent) { + switch (node.parent.type) { + case nodes.NodeType.FunctionDeclaration: + case nodes.NodeType.MixinDeclaration: + case nodes.NodeType.Keyframe: + case nodes.NodeType.VariableDeclaration: + case nodes.NodeType.FunctionParameter: + return DocumentHighlightKind.Write; + } + } + + return DocumentHighlightKind.Read; } function toTwoDigitHex(n: number): string { - const r = n.toString(16); - return r.length !== 2 ? '0' + r : r; + const r = n.toString(16); + return r.length !== 2 ? '0' + r : r; } function getModuleNameFromPath(path: string) { - const firstSlash = path.indexOf('/'); - if (firstSlash === -1) { - return ''; - } - - // If a scoped module (starts with @) then get up until second instance of '/', or to the end of the string for root-level imports. - if (path[0] === '@') { - const secondSlash = path.indexOf('/', firstSlash + 1); - if (secondSlash === -1) { - return path; - } - return path.substring(0, secondSlash); - } - // Otherwise get until first instance of '/' - return path.substring(0, firstSlash); -} + const firstSlash = path.indexOf('/'); + if (firstSlash === -1) { + return ''; + } + + // If a scoped module (starts with @) then get up until second instance of '/', or to the end of the string for root-level imports. + if (path[0] === '@') { + const secondSlash = path.indexOf('/', firstSlash + 1); + if (secondSlash === -1) { + return path; + } + return path.substring(0, secondSlash); + } + // Otherwise get until first instance of '/' + return path.substring(0, firstSlash); +} \ No newline at end of file From ae475defa3401fc42f2c029a1c22d47d89466925 Mon Sep 17 00:00:00 2001 From: Simon Stranks Date: Mon, 30 Oct 2023 19:27:45 +0000 Subject: [PATCH 11/14] fix(formatting): --- .mocharc.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.mocharc.json b/.mocharc.json index 53a15187..fbf679e0 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -1,6 +1,6 @@ { - "ui": "tdd", - "color": true, - "spec": "./lib/umd/test/**/*.test.js", - "recursive": true + "ui": "tdd", + "color": true, + "spec": "./lib/umd/test/**/*.test.js", + "recursive": true } \ No newline at end of file From 01ad668b678a4098238ad96140427612b6efee5a Mon Sep 17 00:00:00 2001 From: Simon Stranks Date: Tue, 31 Oct 2023 14:46:14 +0000 Subject: [PATCH 12/14] fix(formatting): swtiched to default formatter --- src/services/cssNavigation.ts | 36 +++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index 7ef401c5..b7b63add 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -233,7 +233,7 @@ export class CSSNavigation { if (!selectionRange || !containsRange(range, selectionRange)) { selectionRange = Range.create(range.start, range.start); } - + const entry: DocumentSymbol = { name: name || l10n.t(''), kind, @@ -410,23 +410,23 @@ export class CSSNavigation { } // Try resolving the reference from the language configuration alias settings - if (ref && !(await this.fileExists(ref))){ - const rootFolderUri = documentContext.resolveReference('/', documentUri); - if (settings?.paths && rootFolderUri) { - // Specific file reference - if (target in settings.paths){ - return this.mapReference(joinPath(rootFolderUri, settings.paths[target]), isRawLink); - } - // Reference folder - const firstSlash = target.indexOf('/'); - const prefix = `${target.substring(0, firstSlash)}/*`; - if (prefix in settings.paths){ - const aliasPath = (settings.paths[prefix]).slice(0, -1); - let newPath = joinPath(rootFolderUri, aliasPath); - return this.mapReference(newPath = joinPath(newPath, target.substring(prefix.length - 1)), isRawLink); - } - } - } + if (ref && !(await this.fileExists(ref))) { + const rootFolderUri = documentContext.resolveReference('/', documentUri); + if (settings?.paths && rootFolderUri) { + // Specific file reference + if (target in settings.paths) { + return this.mapReference(joinPath(rootFolderUri, settings.paths[target]), isRawLink); + } + // Reference folder + const firstSlash = target.indexOf('/'); + const prefix = `${target.substring(0, firstSlash)}/*`; + if (prefix in settings.paths) { + const aliasPath = (settings.paths[prefix]).slice(0, -1); + let newPath = joinPath(rootFolderUri, aliasPath); + return this.mapReference(newPath = joinPath(newPath, target.substring(prefix.length - 1)), isRawLink); + } + } + } // fall back. it might not exists return ref; From 5915e947cc5ad6fa0b0486044ea77970d65bffc5 Mon Sep 17 00:00:00 2001 From: Simon Stranks Date: Wed, 1 Nov 2023 01:11:51 +0000 Subject: [PATCH 13/14] feat(tests): alias testing for CSS/SCSS @imports --- src/test/css/navigation.test.ts | 23 ++++++++++++- src/test/scss/scssNavigation.test.ts | 51 ++++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/test/css/navigation.test.ts b/src/test/css/navigation.test.ts index c9c89680..406ce573 100644 --- a/src/test/css/navigation.test.ts +++ b/src/test/css/navigation.test.ts @@ -12,7 +12,7 @@ import { colorFrom256RGB, colorFromHSL, colorFromHWB } from '../../languageFacts import { TextDocument, DocumentHighlightKind, Range, Position, TextEdit, Color, - ColorInformation, DocumentLink, SymbolKind, SymbolInformation, Location, LanguageService, Stylesheet, getCSSLanguageService, DocumentSymbol, + ColorInformation, DocumentLink, SymbolKind, SymbolInformation, Location, LanguageService, Stylesheet, getCSSLanguageService, DocumentSymbol, LanguageSettings, } from '../../cssLanguageService'; import { URI } from 'vscode-uri'; @@ -184,6 +184,17 @@ function getCSSLS() { return getCSSLanguageService({ fileSystemProvider: getFsProvider() }); } +function aliasSettings(): LanguageSettings { + return { + "alias": { + "paths": { + "@SingleStylesheet": "/src/assets/styles.css", + "@AssetsDir/*": "/src/assets/*", + } + } + }; +} + suite('CSS - Navigation', () => { suite('Scope', () => { @@ -364,6 +375,16 @@ suite('CSS - Navigation', () => { ]); }); + test('aliased @import links', async function () { + const settings = aliasSettings(); + const ls = getCSSLS(); + ls.configure(settings); + + await assertLinks(ls, '@import "@SingleStylesheet"', [{ range: newRange(8, 27), target: "test://test/src/assets/styles.css"}]); + + await assertLinks(ls, '@import "@AssetsDir/styles.css"', [{ range: newRange(8, 31), target: "test://test/src/assets/styles.css"}]); + }); + test('links in rulesets', async () => { const ls = getCSSLS(); await assertLinks(ls, `body { background-image: url(./foo.jpg)`, [ diff --git a/src/test/scss/scssNavigation.test.ts b/src/test/scss/scssNavigation.test.ts index 08540261..45f66fef 100644 --- a/src/test/scss/scssNavigation.test.ts +++ b/src/test/scss/scssNavigation.test.ts @@ -6,10 +6,10 @@ import * as nodes from '../../parser/cssNodes'; import { assertSymbolsInScope, assertScopesAndSymbols, assertHighlights, assertColorSymbols, assertLinks, newRange, getTestResource, assertDocumentSymbols } from '../css/navigation.test'; -import { getSCSSLanguageService, DocumentLink, TextDocument, SymbolKind } from '../../cssLanguageService'; +import { getSCSSLanguageService, DocumentLink, TextDocument, SymbolKind, LanguageSettings } from '../../cssLanguageService'; import * as assert from 'assert'; import * as path from 'path'; -import { URI, Utils } from 'vscode-uri'; +import { URI } from 'vscode-uri'; import { getFsProvider } from '../testUtil/fsProvider'; import { getDocumentContext } from '../testUtil/documentContext'; @@ -17,8 +17,24 @@ function getSCSSLS() { return getSCSSLanguageService({ fileSystemProvider: getFsProvider() }); } -async function assertDynamicLinks(docUri: string, input: string, expected: DocumentLink[]) { +function aliasSettings(): LanguageSettings { + return { + "alias": { + "paths": { + "@SassStylesheet": "/src/assets/styles.scss", + "@NoUnderscoreDir/*": "/noUnderscore/*", + "@UnderscoreDir/*": "/underscore/*", + "@BothDir/*": "/both/*", + } + } + }; +} + +async function assertDynamicLinks(docUri: string, input: string, expected: DocumentLink[], settings?: LanguageSettings) { const ls = getSCSSLS(); + if (settings) { + ls.configure(settings); + } const document = TextDocument.create(docUri, 'scss', 0, input); const stylesheet = ls.parseStylesheet(document); @@ -177,6 +193,35 @@ suite('SCSS - Navigation', () => { }); + test('SCSS aliased links', async function () { + const fixtureRoot = path.resolve(__dirname, '../../../../src/test/scss/linkFixture'); + const getDocumentUri = (relativePath: string) => { + return URI.file(path.resolve(fixtureRoot, relativePath)).toString(true); + }; + + const settings = aliasSettings(); + const ls = getSCSSLS(); + ls.configure(settings); + + await assertLinks(ls, '@import "@SassStylesheet"', [{ range: newRange(8, 25), target: "test://test/src/assets/styles.scss"}]); + + await assertDynamicLinks(getDocumentUri('./'), `@import '@NoUnderscoreDir/foo'`, [ + { range: newRange(8, 30), target: getDocumentUri('./noUnderscore/foo.scss') } + ], settings); + + await assertDynamicLinks(getDocumentUri('./'), `@import '@UnderscoreDir/foo'`, [ + { range: newRange(8, 28), target: getDocumentUri('./underscore/_foo.scss') } + ], settings); + + await assertDynamicLinks(getDocumentUri('./'), `@import '@BothDir/foo'`, [ + { range: newRange(8, 22), target: getDocumentUri('./both/foo.scss') } + ], settings); + + await assertDynamicLinks(getDocumentUri('./'), `@import '@BothDir/_foo'`, [ + { range: newRange(8, 23), target: getDocumentUri('./both/_foo.scss') } + ], settings); + }); + test('SCSS module file links', async () => { const fixtureRoot = path.resolve(__dirname, '../../../../src/test/scss/linkFixture/module'); const getDocumentUri = (relativePath: string) => { From 896f755bc9918572176ad111c5599d7d9f27d3d6 Mon Sep 17 00:00:00 2001 From: Simon Stranks Date: Wed, 1 Nov 2023 20:58:28 +0000 Subject: [PATCH 14/14] fix(): settings type simplified to one level fix(): folder reference now '/' from '/*' --- src/cssLanguageService.ts | 2 +- src/cssLanguageTypes.ts | 4 ++-- src/services/cssNavigation.ts | 12 ++++++------ src/test/css/navigation.test.ts | 6 ++---- src/test/scss/scssNavigation.test.ts | 10 ++++------ 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/cssLanguageService.ts b/src/cssLanguageService.ts index e175ec6a..ea4017cc 100644 --- a/src/cssLanguageService.ts +++ b/src/cssLanguageService.ts @@ -80,7 +80,7 @@ function createFacade(parser: Parser, completion: CSSCompletion, hover: CSSHover validation.configure(settings); completion.configure(settings?.completion); hover.configure(settings?.hover); - navigation.configure(settings?.alias); + navigation.configure(settings?.importAliases); }, setDataProviders: cssDataManager.setDataProviders.bind(cssDataManager), doValidation: validation.doValidation.bind(validation), diff --git a/src/cssLanguageTypes.ts b/src/cssLanguageTypes.ts index 9c6f95fd..cb5af3ee 100644 --- a/src/cssLanguageTypes.ts +++ b/src/cssLanguageTypes.ts @@ -47,11 +47,11 @@ export interface LanguageSettings { lint?: LintSettings; completion?: CompletionSettings; hover?: HoverSettings; - alias?: AliasSettings; + importAliases?: AliasSettings; } export interface AliasSettings { - paths?: { [key: string]: string }; + [key: string]: string; } export interface HoverSettings { diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index b7b63add..d3d8ec85 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -412,16 +412,16 @@ export class CSSNavigation { // Try resolving the reference from the language configuration alias settings if (ref && !(await this.fileExists(ref))) { const rootFolderUri = documentContext.resolveReference('/', documentUri); - if (settings?.paths && rootFolderUri) { + if (settings && rootFolderUri) { // Specific file reference - if (target in settings.paths) { - return this.mapReference(joinPath(rootFolderUri, settings.paths[target]), isRawLink); + if (target in settings) { + return this.mapReference(joinPath(rootFolderUri, settings[target]), isRawLink); } // Reference folder const firstSlash = target.indexOf('/'); - const prefix = `${target.substring(0, firstSlash)}/*`; - if (prefix in settings.paths) { - const aliasPath = (settings.paths[prefix]).slice(0, -1); + const prefix = `${target.substring(0, firstSlash)}/`; + if (prefix in settings) { + const aliasPath = (settings[prefix]).slice(0, -1); let newPath = joinPath(rootFolderUri, aliasPath); return this.mapReference(newPath = joinPath(newPath, target.substring(prefix.length - 1)), isRawLink); } diff --git a/src/test/css/navigation.test.ts b/src/test/css/navigation.test.ts index 406ce573..8f1a5b0f 100644 --- a/src/test/css/navigation.test.ts +++ b/src/test/css/navigation.test.ts @@ -186,11 +186,9 @@ function getCSSLS() { function aliasSettings(): LanguageSettings { return { - "alias": { - "paths": { + "importAliases": { "@SingleStylesheet": "/src/assets/styles.css", - "@AssetsDir/*": "/src/assets/*", - } + "@AssetsDir/": "/src/assets/", } }; } diff --git a/src/test/scss/scssNavigation.test.ts b/src/test/scss/scssNavigation.test.ts index 45f66fef..597e7236 100644 --- a/src/test/scss/scssNavigation.test.ts +++ b/src/test/scss/scssNavigation.test.ts @@ -19,14 +19,12 @@ function getSCSSLS() { function aliasSettings(): LanguageSettings { return { - "alias": { - "paths": { + "importAliases": { "@SassStylesheet": "/src/assets/styles.scss", - "@NoUnderscoreDir/*": "/noUnderscore/*", - "@UnderscoreDir/*": "/underscore/*", - "@BothDir/*": "/both/*", + "@NoUnderscoreDir/": "/noUnderscore/", + "@UnderscoreDir/": "/underscore/", + "@BothDir/": "/both/", } - } }; }