Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions src/services/cssNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';"
}
}
}
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
65 changes: 59 additions & 6 deletions src/test/scss/scssNavigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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/",
}
};
}

Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
});