diff --git a/README.md b/README.md index 25b7bfe9..ea8bde9f 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ Visit the [release section on GitHub](https://github.com/wkillerud/vscode-scss/r Search for Some Sass (`SomewhatStationery.some-sass`) from the extension installer within VS Code or install from [the Marketplace](https://marketplace.visualstudio.com/items?itemName=SomewhatStationery.some-sass). -If you have SCSS IntelliSense (`mrmlnc.vscode-scss`) installed you should disable or uninstall it. Otherwise the two extensions will both provide hover information and code suggestions. +See [Recommended settings](#recommended-settings-for-visual-studio-code) for some tips on how to tweak code suggestions to your liking. + +Note that if you have SCSS IntelliSense (`mrmlnc.vscode-scss`) installed you should disable or uninstall it. Otherwise the two extensions will both provide hover information and code suggestions. ## Usage @@ -166,7 +168,6 @@ Depending on your project size, you may want to tweak this setting to control ho - JSON key: `somesass.scannerDepth`. - Default: `30`. - #### Stop scanner from following links `@deprecated` @@ -178,7 +179,6 @@ after `@import` becomes CSS-only. - JSON key: `somesass.scanImportedFiles`. - Default: `true`. - ## What this extension does _not_ do - Formating. Consider using [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) if you want automatic formating. diff --git a/fixtures/e2e/completion/main.scss b/fixtures/e2e/completion/main.scss index ca309f72..aa61c818 100644 --- a/fixtures/e2e/completion/main.scss +++ b/fixtures/e2e/completion/main.scss @@ -23,4 +23,9 @@ $fonts: -apple-system; color: ns. @include ns. --runtime-var: var(--other-var, #{ns.}) + font-size: -#{ns.} +} + +@function _multiply($value) { + @return $value * ns.; } diff --git a/fixtures/unit/entry.scss b/fixtures/unit/entry.scss deleted file mode 100644 index df931e85..00000000 --- a/fixtures/unit/entry.scss +++ /dev/null @@ -1,17 +0,0 @@ -@import "mixins/one"; -@import "mixins/two.scss"; -@import "functions/one"; -@import "functions/two.scss"; -@import "variables/one"; -@import "variables/two.scss"; -@import "./includes/b.css"; -@import "**/*.scss"; - -$local: local; - -// local mixin -@mixin two() { - content: $one + two(1, 2); -} - -@include two(); diff --git a/fixtures/unit/functions/one.scss b/fixtures/unit/functions/one.scss deleted file mode 100644 index 76aebe62..00000000 --- a/fixtures/unit/functions/one.scss +++ /dev/null @@ -1,3 +0,0 @@ -@function one() { - @return 1; -} diff --git a/fixtures/unit/functions/two.scss b/fixtures/unit/functions/two.scss deleted file mode 100644 index 6c067943..00000000 --- a/fixtures/unit/functions/two.scss +++ /dev/null @@ -1,3 +0,0 @@ -@function two($a: 1, $b) { - @return 1; -} diff --git a/fixtures/unit/mixins/one.scss b/fixtures/unit/mixins/one.scss deleted file mode 100644 index bf72183e..00000000 --- a/fixtures/unit/mixins/one.scss +++ /dev/null @@ -1,3 +0,0 @@ -@mixin one($a) { - // code -} diff --git a/fixtures/unit/mixins/two.scss b/fixtures/unit/mixins/two.scss deleted file mode 100644 index 51d42f2c..00000000 --- a/fixtures/unit/mixins/two.scss +++ /dev/null @@ -1,3 +0,0 @@ -@mixin two($a: 1, $b) { - width: $a + $b; -} diff --git a/fixtures/unit/scanner/follow-links/namespace/_index.scss b/fixtures/unit/scanner/follow-links/namespace/_index.scss new file mode 100644 index 00000000..6f1c815d --- /dev/null +++ b/fixtures/unit/scanner/follow-links/namespace/_index.scss @@ -0,0 +1 @@ +@forward "./variables" as var-*; diff --git a/fixtures/unit/scanner/follow-links/namespace/_variables.scss b/fixtures/unit/scanner/follow-links/namespace/_variables.scss new file mode 100644 index 00000000..f7f5e698 --- /dev/null +++ b/fixtures/unit/scanner/follow-links/namespace/_variables.scss @@ -0,0 +1 @@ +$var: 1px; diff --git a/fixtures/unit/scanner/follow-links/styles.scss b/fixtures/unit/scanner/follow-links/styles.scss new file mode 100644 index 00000000..266e07c8 --- /dev/null +++ b/fixtures/unit/scanner/follow-links/styles.scss @@ -0,0 +1 @@ +@use "namespace"; diff --git a/fixtures/unit/scanner/self-reference/styles.scss b/fixtures/unit/scanner/self-reference/styles.scss new file mode 100644 index 00000000..d96224db --- /dev/null +++ b/fixtures/unit/scanner/self-reference/styles.scss @@ -0,0 +1,3 @@ +@use "./styles"; + +$var: "hmm"; diff --git a/fixtures/unit/variables/_one.scss b/fixtures/unit/variables/_one.scss deleted file mode 100644 index a4ba7386..00000000 --- a/fixtures/unit/variables/_one.scss +++ /dev/null @@ -1 +0,0 @@ -$one: 1; diff --git a/fixtures/unit/variables/two.scss b/fixtures/unit/variables/two.scss deleted file mode 100644 index b5e0188f..00000000 --- a/fixtures/unit/variables/two.scss +++ /dev/null @@ -1,2 +0,0 @@ -// -$two: 2; diff --git a/package-lock.json b/package-lock.json index 501c16e1..929cc6fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "some-sass", - "version": "2.6.0", + "version": "2.6.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "some-sass", - "version": "2.6.0", + "version": "2.6.2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b00bea98..b9f0213e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "some-sass", "displayName": "Some Sass", "description": "Provides code suggestions, documentation and code navigation for modern SCSS", - "version": "2.6.1", + "version": "2.6.2", "publisher": "SomewhatStationery", "license": "MIT", "engines": { diff --git a/src/server/features/completion/completion-context.ts b/src/server/features/completion/completion-context.ts index 81060839..db0e70b1 100644 --- a/src/server/features/completion/completion-context.ts +++ b/src/server/features/completion/completion-context.ts @@ -17,11 +17,12 @@ export interface CompletionContext { originalExtension: SupportedExtensions; } +const reReturn = /^.*@return/; const rePropertyValue = /.*:\s*/; const reEmptyPropertyValue = /.*:\s*$/; const reQuotedValueInString = /["'](?:[^"'\\]|\\.)*["']/g; const reMixinReference = /.*@include\s+(.*)/; -const reComment = /^.*(\/(\/|\*)|\*)/; +const reComment = /^(.*\/\/|.*\/\*|\s*\*)/; const reSassDoc = /^[\\s]*\/{3}.*$/; const reQuotes = /["']/; const rePartialModuleAtRule = /@(?:use|forward|import) ["']/; @@ -35,9 +36,10 @@ function checkVariableContext( isPropertyValue: boolean, isEmptyValue: boolean, isQuotes: boolean, + isReturn: boolean, isNamespace: boolean, ): boolean { - if (isPropertyValue && !isEmptyValue && !isQuotes) { + if ((isReturn || isPropertyValue) && !isEmptyValue && !isQuotes) { if (isNamespace && word.endsWith(".")) { return true; } @@ -71,10 +73,11 @@ function checkFunctionContext( isPropertyValue: boolean, isEmptyValue: boolean, isQuotes: boolean, + isReturn: boolean, isNamespace: boolean, settings: ISettings, ): boolean { - if (isPropertyValue && !isEmptyValue && !isQuotes) { + if ((isReturn || isPropertyValue) && !isEmptyValue && !isQuotes) { if (isNamespace) { return true; } @@ -114,7 +117,7 @@ function checkNamespaceContext( // Skip #{ if this is interpolation return currentWord.substring( - isInterpolation ? 2 : 0, + isInterpolation ? currentWord.indexOf("{") + 1 : 0, currentWord.indexOf("."), ); } @@ -134,6 +137,7 @@ export function createCompletionContext( const isInterpolation = isInterpolationContext(currentWord); // Information about current position + const isReturn = reReturn.test(textBeforeWord); const isPropertyValue = rePropertyValue.test(textBeforeWord); const isEmptyValue = reEmptyPropertyValue.test(textBeforeWord); const isQuotes = reQuotes.test( @@ -161,6 +165,7 @@ export function createCompletionContext( isPropertyValue, isEmptyValue, isQuotes, + isReturn, Boolean(namespace), ), function: checkFunctionContext( @@ -169,6 +174,7 @@ export function createCompletionContext( isPropertyValue, isEmptyValue, isQuotes, + isReturn, Boolean(namespace), settings, ), diff --git a/src/server/features/completion/completion.ts b/src/server/features/completion/completion.ts index 22b6b206..5927b057 100644 --- a/src/server/features/completion/completion.ts +++ b/src/server/features/completion/completion.ts @@ -314,7 +314,8 @@ function traverseTree( if ( !child.link.target || (child as ScssImport).dynamic || - (child as ScssImport).css + (child as ScssImport).css || + child.link.target === scssDocument.uri ) { continue; } diff --git a/src/server/features/diagnostics/diagnostics.ts b/src/server/features/diagnostics/diagnostics.ts index 6e8d131d..75767722 100644 --- a/src/server/features/diagnostics/diagnostics.ts +++ b/src/server/features/diagnostics/diagnostics.ts @@ -139,7 +139,8 @@ function traverseTree( if ( !child.link.target || (child as ScssImport).dynamic || - (child as ScssImport).css + (child as ScssImport).css || + child.link.target === scssDocument.uri ) { continue; } diff --git a/src/server/features/go-definition/go-definition.ts b/src/server/features/go-definition/go-definition.ts index 94c0f3c2..a8ffd953 100644 --- a/src/server/features/go-definition/go-definition.ts +++ b/src/server/features/go-definition/go-definition.ts @@ -211,7 +211,8 @@ function traverseTree( if ( !child.link.target || (child as ScssImport).dynamic || - (child as ScssImport).css + (child as ScssImport).css || + child.link.target === scssDocument.uri ) { continue; } diff --git a/src/server/features/hover/hover.ts b/src/server/features/hover/hover.ts index cd98fd39..9d576424 100644 --- a/src/server/features/hover/hover.ts +++ b/src/server/features/hover/hover.ts @@ -392,7 +392,8 @@ function traverseTree( if ( !child.link.target || (child as ScssImport).dynamic || - (child as ScssImport).css + (child as ScssImport).css || + child.link.target === scssDocument.uri ) { continue; } diff --git a/src/server/features/signature-help/signature-help.ts b/src/server/features/signature-help/signature-help.ts index 16ecb985..b889ecc6 100644 --- a/src/server/features/signature-help/signature-help.ts +++ b/src/server/features/signature-help/signature-help.ts @@ -246,7 +246,12 @@ export async function doSignatureHelp( ); const sassdoc = applySassDoc(symbol, { - displayOptions: { description: true, deprecated: true, return: true }, + displayOptions: { + description: true, + deprecated: true, + return: true, + parameter: true, + }, }); signatureInfo.documentation = { @@ -347,7 +352,8 @@ function traverseTree( if ( !child.link.target || (child as ScssImport).dynamic || - (child as ScssImport).css + (child as ScssImport).css || + child.link.target === scssDocument.uri ) { continue; } diff --git a/src/server/parser/parser.ts b/src/server/parser/parser.ts index 6ebad3dc..1a20f756 100644 --- a/src/server/parser/parser.ts +++ b/src/server/parser/parser.ts @@ -85,10 +85,27 @@ async function findDocumentSymbols( const partialExists = await fs.exists(partialUri); if (!partialExists) { // We tried to resolve the file as a partial, but it doesn't exist. - continue; + // The target string may be a folder with an index file + // so try looking for it by that name. + const index = ensureIndex(link.target); + const indexUri = URI.parse(index); + const indexExists = await fs.exists(indexUri); + if (!indexExists) { + const partialIndex = ensurePartial(ensureIndex(link.target)); + const partialIndexUri = URI.parse(partialIndex); + const partialIndexExists = await fs.exists(partialIndexUri); + if (!partialIndexExists) { + // We tried, this file doesn't exist + continue; + } else { + link.target = partialIndex; + } + } else { + link.target = index; + } + } else { + link.target = partial; } - - link.target = partial; } const matchUse = reUse.exec(line); @@ -270,6 +287,20 @@ function ensurePartial(target: string): string { return `${path}_${fileName}${extension}`; } +function ensureIndex(target: string): string { + const lastSlash = target.lastIndexOf("/"); + const lastDot = target.lastIndexOf("."); + const fileName = target.substring(lastSlash + 1, lastDot); + + if (fileName.includes("index")) { + return target; + } + + const path = target.slice(0, Math.max(0, lastSlash + 1)); + const extension = target.slice(Math.max(0, lastDot)); + return `${path}/${fileName}/index${extension}`; +} + function urlMatches(url: string, linkTarget: string): boolean { let safeUrl = url; while (/^[./@~]/.exec(safeUrl)) { diff --git a/src/test/e2e/suite/completion/completion.test.ts b/src/test/e2e/suite/completion/completion.test.ts index 3a0b5e52..01c53d65 100644 --- a/src/test/e2e/suite/completion/completion.test.ts +++ b/src/test/e2e/suite/completion/completion.test.ts @@ -209,6 +209,42 @@ describe("SCSS Completion Test", function () { await testCompletion(svelteDocUri, position(28, 40), expectedCompletions); await testCompletion(astroDocUri, position(31, 40), expectedCompletions); }); + + it("Offers namespace completion inside string interpolation with preceeding non-space character", async () => { + const expectedCompletions = [ + { + label: "$var-var-variable", + detail: "Variable declared in _variables.scss", + insertText: '".$var-var-variable"', + filterText: '"ns.$var-var-variable"', + }, + { + label: "fun-fun-function", + detail: "Function declared in _functions.scss", + insertText: '{"_tabstop":1,"value":".fun-fun-function()"}', + }, + ]; + + await testCompletion(docUri, position(26, 20), expectedCompletions); + }); + + it("Offers namespace completion as part of return statement", async () => { + const expectedCompletions = [ + { + label: "$var-var-variable", + detail: "Variable declared in _variables.scss", + insertText: '".$var-var-variable"', + filterText: '"ns.$var-var-variable"', + }, + { + label: "fun-fun-function", + detail: "Function declared in _functions.scss", + insertText: '{"_tabstop":1,"value":".fun-fun-function()"}', + }, + ]; + + await testCompletion(docUri, position(30, 23), expectedCompletions); + }); }); describe("SassDoc Completion Test", () => { diff --git a/src/test/parser/parser.spec.ts b/src/test/parser/parser.spec.ts index 45a0bd8f..b924f628 100644 --- a/src/test/parser/parser.spec.ts +++ b/src/test/parser/parser.spec.ts @@ -136,7 +136,7 @@ describe("Services/Parser", () => { uri: "middle/middle.scss", }); await helpers.makeDocument(storage, ["$tr: 2px;"], fs, { - uri: "moddle/lower/lower.scss", + uri: "middle/lower/lower.scss", }); const document = await helpers.makeDocument( @@ -151,6 +151,23 @@ describe("Services/Parser", () => { strictEqual(uses.length, 3, "expected to find three uses"); }); + + it("should not crash on link to the same document", async () => { + const document = await helpers.makeDocument( + storage, + ['@use "./self";', "$var: 1px;"], + fs, + { + uri: "self.scss", + }, + ); + const symbols = await parseDocument(document, URI.parse(""), fs); + const uses = [...symbols.uses.values()]; + const variables = [...symbols.variables.values()]; + + strictEqual(variables.length, 1, "expected to find one variable"); + strictEqual(uses.length, 0, "expected to find no use link to self"); + }); }); describe("regular expressions", () => { diff --git a/src/test/scanner/scanner-helper.ts b/src/test/scanner/scanner-helper.ts new file mode 100644 index 00000000..d3d663f9 --- /dev/null +++ b/src/test/scanner/scanner-helper.ts @@ -0,0 +1,10 @@ +import * as path from "path"; +import { URI } from "vscode-uri"; + +function getDocPath(p: string) { + return path.resolve(__dirname, "../../../fixtures/unit", p); +} + +export function getUri(p: string) { + return URI.file(getDocPath(p)); +} diff --git a/src/test/scanner/scanner.spec.ts b/src/test/scanner/scanner.spec.ts new file mode 100644 index 00000000..8cd14d74 --- /dev/null +++ b/src/test/scanner/scanner.spec.ts @@ -0,0 +1,44 @@ +import { strictEqual } from "assert"; +import ScannerService from "../../server/scanner"; +import { defaultSettings } from "../../server/settings"; +import StorageService from "../../server/storage"; +import { NodeFileSystem } from "../../shared/node-file-system"; +import { getUri } from "./scanner-helper"; + +const fs = new NodeFileSystem(); + +describe("Services/Parser", () => { + it("should follow links", async () => { + const workspaceUri = getUri("scanner/follow-links/"); + const docUri = getUri("scanner/follow-links/styles.scss"); + const storage = new StorageService(); + const scanner = new ScannerService(storage, fs, defaultSettings); + await scanner.scan([docUri], workspaceUri); + + const documents = [...storage.values()]; + + strictEqual( + documents.length, + 3, + "expected to find three documents in fixtures/unit/scanner/follow-links/", + ); + }); + + it("should not get stuck in loops if the author links a document to itself", async () => { + // Yes, I've had this happen to me during a refactor :D + + const workspaceUri = getUri("scanner/self-reference/"); + const docUri = getUri("scanner/self-reference/styles.scss"); + const storage = new StorageService(); + const scanner = new ScannerService(storage, fs, defaultSettings); + await scanner.scan([docUri], workspaceUri); + + const documents = [...storage.values()]; + + strictEqual( + documents.length, + 1, + "expected to find a document in fixtures/unit/scanner/self-reference/", + ); + }); +});