Skip to content

Commit

Permalink
work on LanguageService.findDocumentSymbols (#291)
Browse files Browse the repository at this point in the history
* SymbolInformation: start offset at selector start

* add LanguageService.findDocumentSymbols2
  • Loading branch information
aeschli authored Sep 1, 2022
1 parent 224b5e4 commit 028dc85
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 65 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@

6.1.0 /
================
* new API `LanguageService.findDocumentSymbols2`, returning `DocumentSymbol[]`

6.0.0 / 2022-05-18
================
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"js-beautify": "^1.14.6",
"mocha": "^10.0.0",
"rimraf": "^3.0.2",
"source-map-support": "^0.5.21",
"typescript": "^4.5.5"
},
"dependencies": {
Expand All @@ -40,8 +41,8 @@
"clean": "rimraf lib",
"remove-sourcemap-refs": "node ./build/remove-sourcemap-refs.js",
"watch": "npm run copy-jsbeautify && tsc -w -p ./src",
"test": "npm run compile && mocha",
"mocha": "mocha",
"test": "npm run compile && npm run mocha",
"mocha": "mocha --require source-map-support/register",
"coverage": "npm run compile && npx nyc --reporter=html --reporter=text mocha",
"lint": "eslint src/**/*.ts",
"update-data": "yarn add @vscode/web-custom-data -D && node ./build/generateData.js",
Expand Down
6 changes: 4 additions & 2 deletions src/cssLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
Diagnostic, Position, CompletionList, Hover, Location, DocumentHighlight, DocumentLink,
SymbolInformation, Range, CodeActionContext, Command, CodeAction, ColorInformation,
Color, ColorPresentation, WorkspaceEdit, FoldingRange, SelectionRange, TextDocument,
ICSSDataProvider, CSSDataV1, HoverSettings, CompletionSettings, TextEdit, CSSFormatConfiguration
ICSSDataProvider, CSSDataV1, HoverSettings, CompletionSettings, TextEdit, CSSFormatConfiguration, DocumentSymbol
} from './cssLanguageTypes';

import { CSSDataManager } from './languageFacts/dataManager';
Expand Down Expand Up @@ -53,6 +53,7 @@ export interface LanguageService {
*/
findDocumentLinks2(document: TextDocument, stylesheet: Stylesheet, documentContext: DocumentContext): Promise<DocumentLink[]>;
findDocumentSymbols(document: TextDocument, stylesheet: Stylesheet): SymbolInformation[];
findDocumentSymbols2(document: TextDocument, stylesheet: Stylesheet): DocumentSymbol[];
doCodeActions(document: TextDocument, range: Range, context: CodeActionContext, stylesheet: Stylesheet): Command[];
doCodeActions2(document: TextDocument, range: Range, context: CodeActionContext, stylesheet: Stylesheet): CodeAction[];
findDocumentColors(document: TextDocument, stylesheet: Stylesheet): ColorInformation[];
Expand Down Expand Up @@ -92,7 +93,8 @@ function createFacade(parser: Parser, completion: CSSCompletion, hover: CSSHover
findDocumentHighlights: navigation.findDocumentHighlights.bind(navigation),
findDocumentLinks: navigation.findDocumentLinks.bind(navigation),
findDocumentLinks2: navigation.findDocumentLinks2.bind(navigation),
findDocumentSymbols: navigation.findDocumentSymbols.bind(navigation),
findDocumentSymbols: navigation.findSymbolInformations.bind(navigation),
findDocumentSymbols2: navigation.findDocumentSymbols.bind(navigation),
doCodeActions: codeActions.doCodeActions.bind(codeActions),
doCodeActions2: codeActions.doCodeActions2.bind(codeActions),
findDocumentColors: navigation.findDocumentColors.bind(navigation),
Expand Down
21 changes: 6 additions & 15 deletions src/parser/cssNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1187,18 +1187,9 @@ export class Document extends BodyDeclaration {
}

export class Medialist extends Node {
private mediums?: Nodelist;

constructor(offset: number, length: number) {
super(offset, length);
}

public getMediums(): Nodelist {
if (!this.mediums) {
this.mediums = new Nodelist(this);
}
return this.mediums;
}
}

export class MediaQuery extends Node {
Expand Down Expand Up @@ -1474,8 +1465,8 @@ export class NumericValue extends Node {

export class VariableDeclaration extends AbstractDeclaration {

private variable: Variable | null = null;
private value: Node | null = null;
private variable: Variable | undefined;
private value: Node | undefined;
public needsSemicolon: boolean = true;

constructor(offset: number, length: number) {
Expand All @@ -1486,7 +1477,7 @@ export class VariableDeclaration extends AbstractDeclaration {
return NodeType.VariableDeclaration;
}

public setVariable(node: Variable | null): node is Variable {
public setVariable(node: Variable | undefined | null): node is Variable {
if (node) {
node.attachTo(this);
this.variable = node;
Expand All @@ -1495,15 +1486,15 @@ export class VariableDeclaration extends AbstractDeclaration {
return false;
}

public getVariable(): Variable | null {
public getVariable(): Variable | undefined {
return this.variable;
}

public getName(): string {
return this.variable ? this.variable.getName() : '';
}

public setValue(node: Node | null): node is Node {
public setValue(node: Node | undefined | null): node is Node {
if (node) {
node.attachTo(this);
this.value = node;
Expand All @@ -1512,7 +1503,7 @@ export class VariableDeclaration extends AbstractDeclaration {
return false;
}

public getValue(): Node | null {
public getValue(): Node | undefined {
return this.value;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/services/cssCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,7 @@ export class CSSCompletion {

public getCompletionsForVariableDeclaration(declaration: nodes.VariableDeclaration, result: CompletionList): CompletionList {
if (this.offset && isDefined(declaration.colonPosition) && this.offset > declaration.colonPosition) {
this.getVariableProposals(declaration.getValue(), result);
this.getVariableProposals(declaration.getValue() || null, result);
}
return result;
}
Expand Down
128 changes: 94 additions & 34 deletions src/services/cssNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import {
Color, ColorInformation, ColorPresentation, DocumentHighlight, DocumentHighlightKind, DocumentLink, Location,
Position, Range, SymbolInformation, SymbolKind, TextEdit, WorkspaceEdit, TextDocument, DocumentContext, FileSystemProvider, FileType
Position, Range, SymbolInformation, SymbolKind, TextEdit, WorkspaceEdit, TextDocument, DocumentContext, FileSystemProvider, FileType, DocumentSymbol
} from '../cssLanguageTypes';
import * as nls from 'vscode-nls';
import * as nodes from '../parser/cssNodes';
Expand All @@ -19,6 +19,8 @@ const localize = nls.loadMessageBundle();

type UnresolvedLinkData = { link: DocumentLink, isRawLink: boolean };

type DocumentSymbolCollector = (name: string, kind: SymbolKind, symbolNodeOrRange: nodes.Node | Range, nameNodeOrRange: nodes.Node | Range | undefined, bodyNode: nodes.Node | undefined) => void;

const startsWithSchemeRegex = /^\w+:\/\//;
const startsWithData = /^data:/;

Expand Down Expand Up @@ -189,57 +191,93 @@ export class CSSNavigation {
return result;
}


public findDocumentSymbols(document: TextDocument, stylesheet: nodes.Stylesheet): SymbolInformation[] {
public findSymbolInformations(document: TextDocument, stylesheet: nodes.Stylesheet): SymbolInformation[] {

const result: SymbolInformation[] = [];

stylesheet.accept((node) => {

const addSymbolInformation = (name: string, kind: SymbolKind, symbolNodeOrRange: nodes.Node | Range) => {
const range = symbolNodeOrRange instanceof nodes.Node ? getRange(symbolNodeOrRange, document) : symbolNodeOrRange;
const entry: SymbolInformation = {
name: null!,
kind: SymbolKind.Class, // TODO@Martin: find a good SymbolKind
location: null!
name,
kind,
location: Location.create(document.uri, range)
};
result.push(entry);
};

this.collectDocumentSymbols(document, stylesheet, addSymbolInformation);

return result;
}

public findDocumentSymbols(document: TextDocument, stylesheet: nodes.Stylesheet): DocumentSymbol[] {
const result: DocumentSymbol[] = [];

const parents: [DocumentSymbol, Range][] = [];

const addDocumentSymbol = (name: string, kind: SymbolKind, symbolNodeOrRange: nodes.Node | Range, nameNodeOrRange: nodes.Node | Range | undefined, bodyNode: nodes.Node | undefined) => {
const range = symbolNodeOrRange instanceof nodes.Node ? getRange(symbolNodeOrRange, document) : symbolNodeOrRange;
const selectionRange = (nameNodeOrRange instanceof nodes.Node ? getRange(nameNodeOrRange, document) : nameNodeOrRange) ?? Range.create(range.start, range.start);
const entry: DocumentSymbol = {
name,
kind,
range,
selectionRange
};
let locationNode: nodes.Node | null = node;
if (node instanceof nodes.Selector) {
entry.name = node.getText();
locationNode = node.findAParent(nodes.NodeType.Ruleset, nodes.NodeType.ExtendsReference);
if (locationNode) {
entry.location = Location.create(document.uri, getRange(locationNode, document));
result.push(entry);
let top = parents.pop();
while (top && !containsRange(top[1], range)) {
top = parents.pop();
}
if (top) {
const topSymbol = top[0];
if (!topSymbol.children) {
topSymbol.children = [];
}
topSymbol.children.push(entry);
parents.push(top); // put back top
} else {
result.push(entry);
}
if (bodyNode) {
parents.push([entry, getRange(bodyNode, document)]);
}
};

this.collectDocumentSymbols(document, stylesheet, addDocumentSymbol);

return result;
}

private collectDocumentSymbols(document: TextDocument, stylesheet: nodes.Stylesheet, collect: DocumentSymbolCollector): void {
stylesheet.accept(node => {
if (node instanceof nodes.RuleSet) {
for (const selector of node.getSelectors().getChildren()) {
if (selector instanceof nodes.Selector) {
const range = Range.create(document.positionAt(selector.offset), document.positionAt(node.end));
collect(selector.getText(), SymbolKind.Class, range, selector, node.getDeclarations());
}
}
return false;
} else if (node instanceof nodes.VariableDeclaration) {
entry.name = (<nodes.VariableDeclaration>node).getName();
entry.kind = SymbolKind.Variable;
collect(node.getName(), SymbolKind.Variable, node, node.getVariable(), undefined);
} else if (node instanceof nodes.MixinDeclaration) {
entry.name = (<nodes.MixinDeclaration>node).getName();
entry.kind = SymbolKind.Method;
collect(node.getName(), SymbolKind.Method, node, node.getIdentifier(), node.getDeclarations());
} else if (node instanceof nodes.FunctionDeclaration) {
entry.name = (<nodes.FunctionDeclaration>node).getName();
entry.kind = SymbolKind.Function;
collect(node.getName(), SymbolKind.Function, node, node.getIdentifier(), node.getDeclarations());
} else if (node instanceof nodes.Keyframe) {
entry.name = localize('literal.keyframes', "@keyframes {0}", (<nodes.Keyframe>node).getName());
const name = localize('literal.keyframes', "@keyframes {0}", node.getName());
collect(name, SymbolKind.Class, node, node.getIdentifier(), node.getDeclarations());
} else if (node instanceof nodes.FontFace) {
entry.name = localize('literal.fontface', "@font-face");
const name = localize('literal.fontface', "@font-face");
collect(name, SymbolKind.Class, node, undefined, node.getDeclarations());
} else if (node instanceof nodes.Media) {
const mediaList = node.getChild(0);
if (mediaList instanceof nodes.Medialist) {
entry.name = '@media ' + mediaList.getText();
entry.kind = SymbolKind.Module;
const name = '@media ' + mediaList.getText();
collect(name, SymbolKind.Module, node, mediaList, node.getDeclarations());
}
}

if (entry.name) {
entry.location = Location.create(document.uri, getRange(locationNode, document));
result.push(entry);
}

return true;
});

return result;
}

public findDocumentColors(document: TextDocument, stylesheet: nodes.Stylesheet): ColorInformation[] {
Expand Down Expand Up @@ -385,6 +423,28 @@ function getRange(node: nodes.Node, document: TextDocument): Range {
return Range.create(document.positionAt(node.offset), document.positionAt(node.end));
}

/**
* Test if `otherRange` is in `range`. If the ranges are equal, will return true.
*/
function containsRange(range: Range, otherRange: Range): boolean {
const otherStartLine = otherRange.start.line, otherEndLine = otherRange.end.line;
const rangeStartLine = range.start.line, rangeEndLine = range.end.line;

if (otherStartLine < rangeStartLine || otherEndLine < rangeStartLine) {
return false;
}
if (otherStartLine > rangeEndLine || otherEndLine > rangeEndLine) {
return false;
}
if (otherStartLine === rangeStartLine && otherRange.start.character < range.start.character) {
return false;
}
if (otherEndLine === rangeEndLine && otherRange.end.character > range.end.character) {
return false;
}
return true;
}

function getHighlightKind(node: nodes.Node): DocumentHighlightKind {

if (node.type === nodes.NodeType.Selector) {
Expand Down
42 changes: 36 additions & 6 deletions src/test/css/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { colorFrom256RGB, colorFromHSL, colorFromHWB } from '../../languageFacts

import {
TextDocument, DocumentHighlightKind, Range, Position, TextEdit, Color,
ColorInformation, DocumentLink, SymbolKind, SymbolInformation, Location, LanguageService, Stylesheet, getCSSLanguageService,
ColorInformation, DocumentLink, SymbolKind, SymbolInformation, Location, LanguageService, Stylesheet, getCSSLanguageService, DocumentSymbol,
} from '../../cssLanguageService';

import { URI } from 'vscode-uri';
Expand Down Expand Up @@ -59,7 +59,7 @@ export async function assertLinks(ls: LanguageService, input: string, expected:
assert.deepEqual(links, expected);
}

export function assertSymbols(ls: LanguageService, input: string, expected: SymbolInformation[], lang: string = 'css') {
export function assertSymbolInfos(ls: LanguageService, input: string, expected: SymbolInformation[], lang: string = 'css') {
let document = TextDocument.create(`test://test/test.${lang}`, lang, 0, input);

let stylesheet = ls.parseStylesheet(document);
Expand All @@ -68,6 +68,15 @@ export function assertSymbols(ls: LanguageService, input: string, expected: Symb
assert.deepEqual(symbols, expected);
}

export function assertDocumentSymbols(ls: LanguageService, input: string, expected: DocumentSymbol[], lang: string = 'css') {
let document = TextDocument.create(`test://test/test.${lang}`, lang, 0, input);

let stylesheet = ls.parseStylesheet(document);

let symbols = ls.findDocumentSymbols2(document, stylesheet);
assert.deepEqual(symbols, expected);
}

export function assertColorSymbols(ls: LanguageService, input: string, ...expected: ColorInformation[]) {
let document = TextDocument.create('test://test/test.css', 'css', 0, input);

Expand Down Expand Up @@ -245,13 +254,34 @@ suite('CSS - Navigation', () => {
});

suite('Symbols', () => {
test('basic symbols', () => {
test('basic symbol infos', () => {
let ls = getCSSLS();
assertSymbolInfos(ls, '.foo {}', [{ name: '.foo', kind: SymbolKind.Class, location: Location.create('test://test/test.css', newRange(0, 7)) }]);
assertSymbolInfos(ls, '.foo:not(.selected) {}', [{ name: '.foo:not(.selected)', kind: SymbolKind.Class, location: Location.create('test://test/test.css', newRange(0, 22)) }]);

// multiple selectors, each range starts with the selector offset
assertSymbolInfos(ls, '.voo.doo, .bar {}', [
{ name: '.voo.doo', kind: SymbolKind.Class, location: Location.create('test://test/test.css', newRange(0, 17)) },
{ name: '.bar', kind: SymbolKind.Class, location: Location.create('test://test/test.css', newRange(10, 17)) },
]);

// Media Query
assertSymbolInfos(ls, '@media screen, print {}', [{ name: '@media screen, print', kind: SymbolKind.Module, location: Location.create('test://test/test.css', newRange(0, 23)) }]);
});

test('basic document symbols', () => {
let ls = getCSSLS();
assertSymbols(ls, '.foo {}', [{ name: '.foo', kind: SymbolKind.Class, location: Location.create('test://test/test.css', newRange(0, 7)) }]);
assertSymbols(ls, '.foo:not(.selected) {}', [{ name: '.foo:not(.selected)', kind: SymbolKind.Class, location: Location.create('test://test/test.css', newRange(0, 22)) }]);
assertDocumentSymbols(ls, '.foo {}', [{ name: '.foo', kind: SymbolKind.Class, range: newRange(0, 7), selectionRange: newRange(0, 4) }]);
assertDocumentSymbols(ls, '.foo:not(.selected) {}', [{ name: '.foo:not(.selected)', kind: SymbolKind.Class, range: newRange(0, 22), selectionRange: newRange(0, 19) }]);

// multiple selectors, each range starts with the selector offset
assertDocumentSymbols(ls, '.voo.doo, .bar {}', [
{ name: '.voo.doo', kind: SymbolKind.Class, range: newRange(0, 17), selectionRange: newRange(0, 8) },
{ name: '.bar', kind: SymbolKind.Class, range: newRange(10, 17), selectionRange: newRange(10, 14) },
]);

// Media Query
assertSymbols(ls, '@media screen, print {}', [{ name: '@media screen, print', kind: SymbolKind.Module, location: Location.create('test://test/test.css', newRange(0, 23)) }]);
assertDocumentSymbols(ls, '@media screen, print {}', [{ name: '@media screen, print', kind: SymbolKind.Module, range: newRange(0, 23), selectionRange: newRange(7, 20) }]);
});
});

Expand Down
Loading

0 comments on commit 028dc85

Please sign in to comment.