Skip to content

Commit

Permalink
SCSS fs provider. Fix microsoft/vscode#58204
Browse files Browse the repository at this point in the history
  • Loading branch information
octref committed Jul 14, 2019
1 parent f386bbd commit b7e999e
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 26 deletions.
7 changes: 5 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"args": [
"--timeout",
"999999",
"--colors"
"--colors"
],
"cwd": "${workspaceRoot}",
"runtimeExecutable": null,
Expand All @@ -19,7 +19,10 @@
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/lib/umd/**"
],
],
"skipFiles": [
"<node_internals>/**"
],
"preLaunchTask": "npm: watch"
}
]
Expand Down
2 changes: 1 addition & 1 deletion src/cssLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export interface LanguageService {
findDefinition(document: TextDocument, position: Position, stylesheet: Stylesheet): Location | null;
findReferences(document: TextDocument, position: Position, stylesheet: Stylesheet): Location[];
findDocumentHighlights(document: TextDocument, position: Position, stylesheet: Stylesheet): DocumentHighlight[];
findDocumentLinks(document: TextDocument, stylesheet: Stylesheet, documentContext: DocumentContext): DocumentLink[];
findDocumentLinks(document: TextDocument, stylesheet: Stylesheet, documentContext: DocumentContext): Promise<DocumentLink[]>;
findDocumentSymbols(document: TextDocument, stylesheet: Stylesheet): SymbolInformation[];
doCodeActions(document: TextDocument, range: Range, context: CodeActionContext, stylesheet: Stylesheet): Command[];
doCodeActions2(document: TextDocument, range: Range, context: CodeActionContext, stylesheet: Stylesheet): CodeAction[];
Expand Down
47 changes: 45 additions & 2 deletions src/cssLanguageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------------*/
'use strict';

import { Range, TextEdit, Position } from 'vscode-languageserver-types';
import { Range, TextEdit, Position, DocumentUri } from 'vscode-languageserver-types';

export { Range, TextEdit, Position };
export { Range, TextEdit, Position, DocumentUri };

export type LintSettings = { [key: string]: any };

Expand Down Expand Up @@ -108,4 +108,47 @@ export interface ICSSDataProvider {
provideAtDirectives(): IAtDirectiveData[];
providePseudoClasses(): IPseudoClassData[];
providePseudoElements(): IPseudoElementData[];
}

export enum FileType {
/**
* The file type is unknown.
*/
Unknown = 0,
/**
* A regular file.
*/
File = 1,
/**
* A directory.
*/
Directory = 2,
/**
* A symbolic link to a file.
*/
SymbolicLink = 64
}

export interface FileStat {
/**
* The type of the file, e.g. is a regular file, a directory, or symbolic link
* to a file.
*/
type: FileType;
/**
* The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
*/
ctime: number;
/**
* The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
*/
mtime: number;
/**
* The size in bytes.
*/
size: number;
}

export interface FileSystemProvider {
stat(uri: DocumentUri): Promise<FileStat>;
}
2 changes: 1 addition & 1 deletion src/services/cssNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export class CSSNavigation {
return result;
}

public findDocumentLinks(document: TextDocument, stylesheet: nodes.Stylesheet, documentContext: DocumentContext): DocumentLink[] {
public async findDocumentLinks(document: TextDocument, stylesheet: nodes.Stylesheet, documentContext: DocumentContext): Promise<DocumentLink[]> {
const result: DocumentLink[] = [];

stylesheet.accept(candidate => {
Expand Down
94 changes: 94 additions & 0 deletions src/services/scssNavigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';

import { CSSNavigation } from './cssNavigation';
import { FileSystemProvider, DocumentContext, FileType } from '../cssLanguageTypes';
import { TextDocument, DocumentLink } from '../cssLanguageService';
import * as nodes from '../parser/cssNodes';

export class SCSSNavigation extends CSSNavigation {
constructor(private fileSystemProvider?: FileSystemProvider) {
super();
}

public async findDocumentLinks(
document: TextDocument,
stylesheet: nodes.Stylesheet,
documentContext: DocumentContext
): Promise<DocumentLink[]> {
const links = await super.findDocumentLinks(document, stylesheet, documentContext);
const fsProvider = this.fileSystemProvider;

/**
* Validate and correct links
*/
for (let i = 0; i < links.length; i++) {
if (links[i].target.endsWith('.scss') && !(await fileExists(links[i].target))) {
const { originalBasename, normalizedBasename, normalizedUri, withBasename } = toNormalizedUri(links[i].target);
// a.scss case
if (originalBasename === normalizedBasename && await fileExists(withBasename('_' + originalBasename))) {
links[i].target = withBasename('_' + originalBasename);
continue;
}
// _a.scss case
else if (originalBasename === '_' + normalizedBasename && await fileExists(normalizedUri)) {
links[i].target = normalizedUri;
continue;
}

// a/index.scss and a/_index.scss case
const indexUri = withBasename(normalizedBasename.replace('.scss', '/index.scss'));
const _indexUri = withBasename(normalizedBasename.replace('.scss', '/_index.scss'));

if (await fileExists(indexUri)) {
links[i].target = indexUri;
} else if (await fileExists(_indexUri)) {
links[i].target = _indexUri;
}
}
}

return links;

function toNormalizedUri(uri: string) {
const uriFragments = uri.split('/');
let normalizedBasename = uriFragments[uriFragments.length - 1];
if (normalizedBasename.startsWith('_')) {
normalizedBasename = normalizedBasename.slice(1);
}
if (!normalizedBasename.endsWith('.scss')) {
normalizedBasename += '.scss';
}

const normalizedUri = [...uriFragments.slice(0, -1), normalizedBasename].join('/');
return {
originalBasename: uriFragments[uriFragments.length - 1],
normalizedUri,
normalizedBasename,
withBasename(newBaseName: string) {
return [...uriFragments.slice(0, -1), newBaseName].join('/');
}
};
}

async function fileExists(uri: string) {
if (!fsProvider) {
return false;
}

try {
const stat = await fsProvider.stat(uri);
if (stat.type === FileType.Unknown && stat.size === -1) {
return false;
}

return true;
} catch (err) {
return false;
}
}
}
}
34 changes: 17 additions & 17 deletions src/test/css/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,20 @@ export function assertHighlights(p: Parser, input: string, marker: string, expec
assert.equal(nWrites, expectedWrites, input);
}

function getDocumentContext(documentUrl: string): DocumentContext {
export function getDocumentContext(documentUrl: string): DocumentContext {
return {
resolveReference: (ref, base = documentUrl) => {
return url.resolve(base, ref);
}
};
}

export function assertLinks(p: Parser, input: string, expected: DocumentLink[], lang: string = 'css') {
export async function assertLinks(p: Parser, input: string, expected: DocumentLink[], lang: string = 'css') {
let document = TextDocument.create(`test://test/test.${lang}`, lang, 0, input);

let stylesheet = p.parseStylesheet(document);

let links = new CSSNavigation().findDocumentLinks(document, stylesheet, getDocumentContext(document.uri));
let links = await new CSSNavigation().findDocumentLinks(document, stylesheet, getDocumentContext(document.uri));
assert.deepEqual(links, expected);
}

Expand Down Expand Up @@ -292,51 +292,51 @@ suite('CSS - Navigation', () => {

suite('Links', () => {

test('basic @import links', () => {
test('basic @import links', async () => {
let p = new Parser();
assertLinks(p, `@import 'foo.css';`, [
await assertLinks(p, `@import 'foo.css';`, [
{ range: newRange(8, 17), target: 'test://test/foo.css' }
]);

assertLinks(p, `@import './foo.css';`, [
await assertLinks(p, `@import './foo.css';`, [
{ range: newRange(8, 19), target: 'test://test/foo.css' }
]);

assertLinks(p, `@import '../foo.css';`, [
await assertLinks(p, `@import '../foo.css';`, [
{ range: newRange(8, 20), target: 'test://foo.css' }
]);
});

test('complex @import links', () => {
test('complex @import links', async () => {
let p = new Parser();
assertLinks(p, `@import url("foo.css") print;`, [
await assertLinks(p, `@import url("foo.css") print;`, [
{ range: newRange(12, 21), target: 'test://test/foo.css' }
]);

assertLinks(p, `@import url("chrome://downloads")`, [
await assertLinks(p, `@import url("chrome://downloads")`, [
{ range: newRange(12, 32), target: 'chrome://downloads' }
]);

assertLinks(p, `@import url('landscape.css') screen and (orientation:landscape);`, [
await assertLinks(p, `@import url('landscape.css') screen and (orientation:landscape);`, [
{ range: newRange(12, 27), target: 'test://test/landscape.css' }
]);
});

test('links in rulesets', () => {
test('links in rulesets', async () => {
let p = new Parser();
assertLinks(p, `body { background-image: url(./foo.jpg)`, [
await assertLinks(p, `body { background-image: url(./foo.jpg)`, [
{ range: newRange(29, 38), target: 'test://test/foo.jpg' }
]);

assertLinks(p, `body { background-image: url('./foo.jpg')`, [
await assertLinks(p, `body { background-image: url('./foo.jpg')`, [
{ range: newRange(29, 40), target: 'test://test/foo.jpg' }
]);
});

test('No links with empty range', () => {
test('No links with empty range', async () => {
let p = new Parser();
assertLinks(p, `body { background-image: url()`, []);
assertLinks(p, `@import url();`, []);
await assertLinks(p, `body { background-image: url()`, []);
await assertLinks(p, `@import url();`, []);
});

});
Expand Down
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
87 changes: 84 additions & 3 deletions src/test/scss/scssNavigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,62 @@

import { SCSSParser } from '../../parser/scssParser';
import * as nodes from '../../parser/cssNodes';
import { assertSymbolsInScope, assertScopesAndSymbols, assertHighlights, assertColorSymbols, assertLinks, newRange } from '../css/navigation.test';
import { getSCSSLanguageService } from '../../cssLanguageService';
import { assertSymbolsInScope, assertScopesAndSymbols, assertHighlights, assertColorSymbols, assertLinks, newRange, getDocumentContext } from '../css/navigation.test';
import { getSCSSLanguageService, DocumentLink, TextDocument } from '../../cssLanguageService';
import * as assert from 'assert';
import { FileSystemProvider, FileType } from '../../cssLanguageTypes';
import { stat as fsStat } from 'fs';
import { SCSSNavigation } from '../../services/scssNavigation';
import * as path from 'path';

async function assertDynamicLinks(docUri: string, input: string, expected: DocumentLink[]) {
const p = new SCSSParser();
const document = TextDocument.create(docUri, 'scss', 0, input);

const stylesheet = p.parseStylesheet(document);

const links = await new SCSSNavigation(getFsProvider()).findDocumentLinks(document, stylesheet, getDocumentContext(document.uri));
assert.deepEqual(links, expected);
}

function getFsProvider(): FileSystemProvider {
return {
stat(uri: string) {
return new Promise((c, e) => {
fsStat(uri, (err, stats) => {
if (err) {
if (err.code === 'ENOENT') {
return c({
type: FileType.Unknown,
ctime: -1,
mtime: -1,
size: -1
});
} else {
return e(err);
}
}

let type = FileType.Unknown;
if (stats.isFile()) {
type = FileType.File;
} else if (stats.isDirectory) {
type = FileType.Directory;
} else if (stats.isSymbolicLink) {
type = FileType.SymbolicLink;
}

c({
type,
ctime: stats.ctime.getTime(),
mtime: stats.mtime.getTime(),
size: stats.size
});
});
});
}
};
}

suite('SCSS - Navigation', () => {

Expand Down Expand Up @@ -138,6 +192,34 @@ suite('SCSS - Navigation', () => {
], 'scss');
});

test('SCSS partial file dynamic links', async () => {
const fixtureRoot = path.resolve(__dirname, '../../../../src/test/scss/linkFixture');

await assertDynamicLinks(path.resolve(fixtureRoot, './noUnderscore/index.scss'), `@import 'foo'`, [
{ range: newRange(8, 13), target: '/Users/pine/Code/work/css/css-service2/src/test/scss/linkFixture/noUnderscore/foo.scss' }
]);

await assertDynamicLinks(path.resolve(fixtureRoot, './underscore/index.scss'), `@import 'foo'`, [
{ range: newRange(8, 13), target: '/Users/pine/Code/work/css/css-service2/src/test/scss/linkFixture/underscore/_foo.scss' }
]);

await assertDynamicLinks(path.resolve(fixtureRoot, './both/index.scss'), `@import 'foo'`, [
{ range: newRange(8, 13), target: '/Users/pine/Code/work/css/css-service2/src/test/scss/linkFixture/both/_foo.scss' }
]);

await assertDynamicLinks(path.resolve(fixtureRoot, './both/index.scss'), `@import '_foo'`, [
{ range: newRange(8, 14), target: '/Users/pine/Code/work/css/css-service2/src/test/scss/linkFixture/both/_foo.scss' }
]);

await assertDynamicLinks(path.resolve(fixtureRoot, './index/index.scss'), `@import 'foo'`, [
{ range: newRange(8, 13), target: '/Users/pine/Code/work/css/css-service2/src/test/scss/linkFixture/index/foo/index.scss' }
]);

await assertDynamicLinks(path.resolve(fixtureRoot, './index/index.scss'), `@import 'bar'`, [
{ range: newRange(8, 13), target: '/Users/pine/Code/work/css/css-service2/src/test/scss/linkFixture/index/bar/_index.scss' }
]);
});

test('SCSS straight links', () => {
const p = new SCSSParser();

Expand All @@ -160,7 +242,6 @@ suite('SCSS - Navigation', () => {
});
});


suite('Color', () => {

test('color symbols', () => {
Expand Down

0 comments on commit b7e999e

Please sign in to comment.