Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add findDocumentSymbols #7

Merged
merged 1 commit into from Feb 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -12,7 +12,8 @@ and the Monaco editor.
- *doComplete* provides completion proposals for a given location.
- *findDocumentHighlights* provides the highlighted symbols for a given position
- *format* formats the code at the given range.
- *findDocumentLinks* finds al links in the document
- *findDocumentLinks* finds all links in the document
- *findDocumentSymbols* finds all the symbols in the document

Installation
------------
Expand Down
5 changes: 4 additions & 1 deletion src/htmlLanguageService.ts
Expand Up @@ -11,6 +11,7 @@ import {doHover} from './services/htmlHover';
import {format} from './services/htmlFormatter';
import {findDocumentLinks} from './services/htmlLinks';
import {findDocumentHighlights} from './services/htmlHighlighting';
import {findDocumentSymbols} from './services/htmlSymbolsProvider';
import {TextDocument, Position, CompletionItem, CompletionList, Hover, Range, SymbolInformation, Diagnostic, TextEdit, DocumentHighlight, FormattingOptions, MarkedString, DocumentLink } from 'vscode-languageserver-types';

export {TextDocument, Position, CompletionItem, CompletionList, Hover, Range, SymbolInformation, Diagnostic, TextEdit, DocumentHighlight, FormattingOptions, MarkedString, DocumentLink };
Expand Down Expand Up @@ -113,6 +114,7 @@ export interface LanguageService {
doHover(document: TextDocument, position: Position, htmlDocument: HTMLDocument): Hover;
format(document: TextDocument, range: Range, options: HTMLFormatConfiguration): TextEdit[];
findDocumentLinks(document: TextDocument, documentContext: DocumentContext): DocumentLink[];
findDocumentSymbols(document: TextDocument, htmlDocument: HTMLDocument): SymbolInformation[];
}

export function getLanguageService(): LanguageService {
Expand All @@ -123,6 +125,7 @@ export function getLanguageService(): LanguageService {
doHover,
format,
findDocumentHighlights,
findDocumentLinks
findDocumentLinks,
findDocumentSymbols
};
}
19 changes: 14 additions & 5 deletions src/parser/htmlParser.ts
Expand Up @@ -12,7 +12,7 @@ export class Node {
public tag: string;
public closed: boolean;
public endTagStart: number;
public attributeNames: string[];
public attributes: {[name: string]: string};
constructor(public start: number, public end: number, public children: Node[], public parent: Node) {
}
public isSameTag(tagInLowerCase: string) {
Expand Down Expand Up @@ -63,6 +63,7 @@ export function parse(text: string): HTMLDocument {
let htmlDocument = new Node(0, text.length, [], null);
let curr = htmlDocument;
let endTagStart: number = -1;
let pendingAttribute: string = null;
let token = scanner.scan();
while (token !== TokenType.EOS) {
switch (token) {
Expand Down Expand Up @@ -110,11 +111,19 @@ export function parse(text: string): HTMLDocument {
}
break;
case TokenType.AttributeName:
let attributeNames = curr.attributeNames;
if (!attributeNames) {
curr.attributeNames = attributeNames = [];
let attributeName = pendingAttribute = scanner.getTokenText();
let attributes = curr.attributes;
if (!attributes) {
curr.attributes = attributes = {};
}
attributes[pendingAttribute] = null; // Support valueless attributes such as 'checked'
break;
case TokenType.AttributeValue:
let value = scanner.getTokenText();
if (attributes && pendingAttribute) {
attributes[pendingAttribute] = value;
pendingAttribute = null;
}
attributeNames.push(scanner.getTokenText());
break;
}
token = scanner.scan();
Expand Down
57 changes: 57 additions & 0 deletions src/services/htmlSymbolsProvider.ts
@@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* 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 {TextDocument, Position, Location, Range, SymbolInformation, SymbolKind} from 'vscode-languageserver-types';
import {HTMLDocument, Node} from '../parser/htmlParser';
import {TokenType, createScanner, ScannerState} from '../parser/htmlScanner';

export function findDocumentSymbols(document: TextDocument, htmlDocument: HTMLDocument): SymbolInformation[] {
let symbols = <SymbolInformation[]>[];

htmlDocument.roots.forEach(node => {
provideFileSymbolsInternal(document, node, '', symbols)
});

return symbols;
}

function provideFileSymbolsInternal(document: TextDocument, node: Node, container: string, symbols: SymbolInformation[]): void {

let name = nodeToName(node);
let location = Location.create(document.uri, Range.create(document.positionAt(node.start), document.positionAt(node.end)));
let symbol = <SymbolInformation> {
name: name,
location: location,
containerName: container,
kind: <SymbolKind>SymbolKind.Field
}

symbols.push(symbol);

node.children.forEach(child => {
provideFileSymbolsInternal(document, child, name, symbols);
});
}


function nodeToName(node: Node): string {
let name = node.tag;

if (node.attributes) {
let id = node.attributes['id'];
let classes = node.attributes['class'];

if (id) {
name += `#${id.replace(/[\"\']/g, '')}`;
}

if (classes) {
name += classes.replace(/[\"\']/g, '').split(/\s+/).map(className => `.${className}`).join('');
}
}

return name;
}
40 changes: 39 additions & 1 deletion src/test/parser.test.ts
Expand Up @@ -13,6 +13,10 @@ suite('HTML Parser', () => {
return { tag: node.tag, start: node.start, end: node.end, endTagStart: node.endTagStart, closed: node.closed, children: node.children.map(toJSON) };
}

function toJSONWithAttributes(node: Node) {
return { tag: node.tag, attributes: node.attributes, children: node.children.map(toJSONWithAttributes) };
}

function assertDocument(input: string, expected: any) {
let document = parse(input);
assert.deepEqual(document.roots.map(toJSON), expected);
Expand All @@ -24,6 +28,11 @@ suite('HTML Parser', () => {
assert.equal(node ? node.tag : '', expectedTag, "offset " + offset);
}

function assertAttributes(input: string, expected: any) {
let document = parse(input);
assert.deepEqual(document.roots.map(toJSONWithAttributes), expected);
}

test('Simple', () => {
assertDocument('<html></html>', [{ tag: 'html', start: 0, end: 13, endTagStart: 6, closed: true, children: [] }]);
assertDocument('<html><body></body></html>', [{ tag: 'html', start: 0, end: 26, endTagStart: 19, closed: true, children: [{ tag: 'body', start: 6, end: 19, endTagStart: 12, closed: true, children: [] }] }]);
Expand All @@ -44,6 +53,7 @@ suite('HTML Parser', () => {
]
}]);
});

test('MissingTags', () => {
assertDocument('</meta>', []);
assertDocument('<div></div></div>', [{ tag: 'div', start: 0, end: 11, endTagStart: 5, closed: true, children: [] }]);
Expand All @@ -52,7 +62,6 @@ suite('HTML Parser', () => {
assertDocument('<h1><div><span></h1>', [{ tag: 'h1', start: 0, end: 20, endTagStart: 15, closed: true, children: [{ tag: 'div', start: 4, end: 15, endTagStart: void 0, closed: false, children: [{ tag: 'span', start: 9, end: 15, endTagStart: void 0, closed: false, children: [] }] }] }]);
});


test('FindNodeBefore', () => {
let str = '<div><input type="button"><span><br><hr></span></div>';
assertNodeBefore(str, 0, void 0);
Expand Down Expand Up @@ -82,4 +91,33 @@ suite('HTML Parser', () => {
assertNodeBefore(str, 21, 'div');
});

test('Attributes', () => {
let str = '<div class="these are my-classes" id="test"><span aria-describedby="test"></span></div>'
assertAttributes(str, [{
tag: 'div',
attributes: {
class: '"these are my-classes"',
id: '"test"'
},
children: [{
tag: 'span',
attributes: {
'aria-describedby': '"test"'
},
children: []
}]
}]);
});

test('Attributes without value', () => {
let str = '<div checked id="test"></div>'
assertAttributes(str, [{
tag: 'div',
attributes: {
checked: null,
id: '"test"'
},
children: []
}]);
});
});
37 changes: 37 additions & 0 deletions src/test/symbols.test.ts
@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* 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 * as assert from 'assert';
import * as htmlLanguageService from '../htmlLanguageService';

import { TextDocument, SymbolInformation, SymbolKind, Location, Range, Position } from 'vscode-languageserver-types';

suite('HTML Symbols', () => {

const TEST_URI = "test://test/test.html";

function asPromise<T>(result: T): Promise<T> {
return Promise.resolve(result);
}

let assertSymbols = function (symbols: SymbolInformation[], expected: SymbolInformation[]) {
assert.deepEqual(symbols, expected);
}

let testSymbolsFor = function(value: string, expected: SymbolInformation[]) {
let ls = htmlLanguageService.getLanguageService();
let document = TextDocument.create(TEST_URI, 'html', 0, value);
let htmlDoc = ls.parseHTMLDocument(document);
let symbols = ls.findDocumentSymbols(document, htmlDoc);
assertSymbols(symbols, expected);
}

test('Simple', () => {
testSymbolsFor('<div></div>', [<SymbolInformation>{ containerName: '', name: 'div', kind: <SymbolKind>SymbolKind.Field, location: Location.create(TEST_URI, Range.create(0, 0, 0, 11)) }]);
testSymbolsFor('<div><input checked id="test" class="checkbox"></div>', [{ containerName: '', name: 'div', kind: <SymbolKind>SymbolKind.Field, location: Location.create(TEST_URI, Range.create(0, 0, 0, 53)) },
{ containerName: 'div', name: 'input#test.checkbox', kind: <SymbolKind>SymbolKind.Field, location: Location.create(TEST_URI, Range.create(0, 5, 0, 47)) }]);
});
})