diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index 663f9125..27616ecf 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -416,8 +416,8 @@ export class CSSNavigation { 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); + const remainder = ref.length > moduleName.length ? ref.slice(moduleName.length + 1) : ''; // skip the '/', bare import + return remainder ? joinPath(modulePath, remainder) : modulePath; // e.g. "@import 'bootstrap';" } } } @@ -439,7 +439,18 @@ export class CSSNavigation { return this.mapReference(await this.resolveModuleReference(target, documentUri, documentContext), isRawLink); } - const ref = await this.mapReference(documentContext.resolveReference(target, documentUri), isRawLink); + // Treat bare module names (“bootstrap/...”) like sass-loader does + if (this.resolveModuleReferences && importIsBare(target)) { + const resolvedModuleRef = await this.resolveModuleReference(target, documentUri, documentContext); + const moduleRef = await this.mapReference(resolvedModuleRef, isRawLink); + + if (moduleRef != null) { + return moduleRef; + } + } + + 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) @@ -590,6 +601,12 @@ function toTwoDigitHex(n: number): string { return r.length !== 2 ? '0' + r : r; } +function importIsBare(target: string): boolean { + return !target.startsWith('.') // not ./ or ../ + && !target.startsWith('/') // not workspace-absolute + && !startsWithSchemeRegex.test(target); // not a scheme (file://, http://, etc.) +} + export function getModuleNameFromPath(path: string) { const firstSlash = path.indexOf('/'); if (firstSlash === -1) { diff --git a/src/test/scss/scssNavigation.test.ts b/src/test/scss/scssNavigation.test.ts index f667e6d7..8220d822 100644 --- a/src/test/scss/scssNavigation.test.ts +++ b/src/test/scss/scssNavigation.test.ts @@ -6,7 +6,7 @@ import * as nodes from '../../parser/cssNodes'; import { assertSymbolsInScope, assertScopesAndSymbols, assertHighlights, assertColorSymbols, assertLinks, newRange, getTestResource, assertDocumentSymbols } from '../css/navigation.test'; -import { getSCSSLanguageService, DocumentLink, TextDocument, SymbolKind, LanguageSettings } from '../../cssLanguageService'; +import { getSCSSLanguageService, DocumentLink, TextDocument, SymbolKind, LanguageSettings, DocumentContext } from '../../cssLanguageService'; import * as assert from 'assert'; import * as path from 'path'; import { URI } from 'vscode-uri'; @@ -20,11 +20,11 @@ function getSCSSLS() { function aliasSettings(): LanguageSettings { return { "importAliases": { - "@SassStylesheet": "/src/assets/styles.scss", - "@NoUnderscoreDir/": "/noUnderscore/", - "@UnderscoreDir/": "/underscore/", - "@BothDir/": "/both/", - } + "@SassStylesheet": "/src/assets/styles.scss", + "@NoUnderscoreDir/": "/noUnderscore/", + "@UnderscoreDir/": "/underscore/", + "@BothDir/": "/both/", + } }; } @@ -57,6 +57,21 @@ async function assertNoDynamicLinks(docUri: string, input: string, extecedTarget } +function createDocument(contents: string, uri = 'file:///test.scss') { + return TextDocument.create(uri, 'scss', 0, contents); +} + +const dummyContext: DocumentContext = { + resolveReference: (ref: string, _base: string) => ref +}; + +async function getLinks(contents: string) { + const ls = getSCSSLS(); + const doc = createDocument(contents); + const stylesheet = ls.parseStylesheet(doc); + return ls.findDocumentLinks2(doc, stylesheet, dummyContext); +} + suite('SCSS - Navigation', () => { suite('Scopes and Symbols', () => { @@ -353,4 +368,42 @@ suite('SCSS - Navigation', () => { }); }); + suite('URL Scheme Imports', () => { + + test('http scheme import is treated as absolute URL, not bare import', async () => { + const links = await getLinks(`@import "http://example.com/foo.css";`); + assert.strictEqual(links.length, 1); + assert.strictEqual(links[0].target, 'http://example.com/foo.css'); + }); + + test('https scheme import is treated as absolute URL, not bare import', async () => { + const links = await getLinks(`@import "https://cdn.example.com/reset.css";`); + assert.strictEqual(links.length, 1); + assert.strictEqual(links[0].target, 'https://cdn.example.com/reset.css'); + }); + + test('file scheme import is treated as absolute URL, not bare import', async () => { + const links = await getLinks(`@import "file:///Users/test/project/styles/base.scss";`); + assert.strictEqual(links.length, 1); + assert.strictEqual(links[0].target, 'file:///Users/test/project/styles/base.scss'); + }); + + test('custom scheme import (vscode-resource) is treated as absolute URL, not bare import', async () => { + const links = await getLinks(`@import "vscode-resource://file/some.css";`); + assert.strictEqual(links.length, 1); + assert.strictEqual(links[0].target, 'vscode-resource://file/some.css'); + }); + }); + + suite('Bare Imports', () => { + + test('resolves bare import path on Windows', async () => { + const ls = getSCSSLS(); + + const doc = TextDocument.create('file:///c:/proj/app.scss', 'scss', 1, "@import 'bootstrap/scss/variables';"); + const links = await ls.findDocumentLinks2(doc, ls.parseStylesheet(doc), getDocumentContext('c:/proj')); + const expected = URI.file('c:/proj/node_modules/bootstrap/scss/_variables.scss').toString(); + assert.strictEqual(links[0].target, expected); + }); + }); });