diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index 4192dfd2..0a74a75d 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -399,8 +399,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';" } } } @@ -422,7 +422,22 @@ 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 + const startsWithSchemeRegex = /^\w[\w\d+.-]:/; + + const isBareImport = !target.startsWith('.') // not ./ or ../ + && !target.startsWith('/') // not workspace-absolute + && !startsWithSchemeRegex.test(target); // not a scheme (file://, http://, etc.) + + if (isBareImport) { + const moduleRef = await this.mapReference( + await this.resolveModuleReference(target, documentUri, documentContext), + isRawLink); + if (moduleRef) { 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) @@ -589,4 +604,4 @@ export 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/scss/schemeImportLinks.test.ts b/src/test/scss/schemeImportLinks.test.ts new file mode 100644 index 00000000..fe2b08fe --- /dev/null +++ b/src/test/scss/schemeImportLinks.test.ts @@ -0,0 +1,46 @@ +import * as assert from 'assert'; +import { getSCSSLanguageService } from '../../scssLanguageService'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { DocumentContext } from '../../cssLanguageTypes'; + +function createDocument(contents: string, uri = 'file:///test.scss') { + return TextDocument.create(uri, 'scss', 0, contents); +} + +const dummyContext: DocumentContext = { + resolveReference: (ref: string, _base: string) => ref +}; + +const ls = getSCSSLanguageService(); + +async function getLinks(contents: string) { + const doc = createDocument(contents); + const stylesheet = ls.parseStylesheet(doc); + return ls.findDocumentLinks2(doc, stylesheet, dummyContext); +} + +describe('SCSS Navigation – scheme URL imports', () => { + it('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'); + }); + + it('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'); + }); + + it('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'); + }); + + it('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'); + }); +}); diff --git a/src/test/scss/scssNavigation-node-modules.test.ts b/src/test/scss/scssNavigation-node-modules.test.ts new file mode 100644 index 00000000..8bec199a --- /dev/null +++ b/src/test/scss/scssNavigation-node-modules.test.ts @@ -0,0 +1,18 @@ +import { assert } from 'chai'; +import { getSCSSLanguageService } from '../../cssLanguageService'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { FileType, getNodeFSRequestService } from '../../nodeFs'; +import { URI } from 'vscode-uri'; + +const ls = getSCSSLanguageService(); +const mockFS = getNodeFSRequestService(); + +describe('SCSS link navigation – node_modules', () => { + it('resolves bootstrap path on Windows', async () => { + const doc = TextDocument.create('file:///c:/proj/app.scss', 'scss', 1, + "@import 'bootstrap/scss/variables';"); + const links = await ls.findDocumentLinks2(doc, ls.parseStylesheet(doc), {}, mockFS); + const expected = URI.file('c:/proj/node_modules/bootstrap/scss/_variables.scss').toString(); + assert.strictEqual(links[0].target, expected); + }); +});