diff --git a/src/cssLanguageService.ts b/src/cssLanguageService.ts index a9d9b676..ea4017cc 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?.importAliases); }, setDataProviders: cssDataManager.setDataProviders.bind(cssDataManager), doValidation: validation.doValidation.bind(validation), diff --git a/src/cssLanguageTypes.ts b/src/cssLanguageTypes.ts index 5c2933a5..cb5af3ee 100644 --- a/src/cssLanguageTypes.ts +++ b/src/cssLanguageTypes.ts @@ -47,8 +47,12 @@ export interface LanguageSettings { lint?: LintSettings; completion?: CompletionSettings; hover?: HoverSettings; + importAliases?: AliasSettings; } +export interface AliasSettings { + [key: string]: string; +} export interface HoverSettings { documentation?: boolean; diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index 9b52c698..d3d8ec85 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -5,7 +5,7 @@ 'use strict'; import { - Color, ColorInformation, ColorPresentation, DocumentHighlight, DocumentHighlightKind, DocumentLink, Location, + AliasSettings, Color, ColorInformation, ColorPresentation, DocumentHighlight, DocumentHighlightKind, DocumentLink, Location, Position, Range, SymbolInformation, SymbolKind, TextEdit, WorkspaceEdit, TextDocument, DocumentContext, FileSystemProvider, FileType, DocumentSymbol } from '../cssLanguageTypes'; import * as l10n from '@vscode/l10n'; @@ -24,10 +24,15 @@ const startsWithSchemeRegex = /^\w+:\/\//; const startsWithData = /^data:/; export class CSSNavigation { + protected defaultSettings?: AliasSettings; 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); @@ -228,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, @@ -376,7 +381,7 @@ export class CSSNavigation { return target; } - protected async resolveReference(target: string, documentUri: string, documentContext: DocumentContext, isRawLink = false): Promise { + 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) @@ -403,6 +408,26 @@ export class CSSNavigation { return moduleReference; } } + + // Try resolving the reference from the language configuration alias settings + if (ref && !(await this.fileExists(ref))) { + const rootFolderUri = documentContext.resolveReference('/', documentUri); + if (settings && rootFolderUri) { + // Specific file reference + 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) { + const aliasPath = (settings[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; } @@ -522,4 +547,4 @@ function getModuleNameFromPath(path: string) { } // Otherwise get until first instance of '/' return path.substring(0, firstSlash); -} +} \ No newline at end of file diff --git a/src/test/css/navigation.test.ts b/src/test/css/navigation.test.ts index c9c89680..8f1a5b0f 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,15 @@ function getCSSLS() { return getCSSLanguageService({ fileSystemProvider: getFsProvider() }); } +function aliasSettings(): LanguageSettings { + return { + "importAliases": { + "@SingleStylesheet": "/src/assets/styles.css", + "@AssetsDir/": "/src/assets/", + } + }; +} + suite('CSS - Navigation', () => { suite('Scope', () => { @@ -364,6 +373,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..597e7236 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,22 @@ function getSCSSLS() { return getSCSSLanguageService({ fileSystemProvider: getFsProvider() }); } -async function assertDynamicLinks(docUri: string, input: string, expected: DocumentLink[]) { +function aliasSettings(): LanguageSettings { + return { + "importAliases": { + "@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 +191,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) => {