Files
vscode/src/vs/languages/css/common/css.ts
T

419 lines
16 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* 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 WinJS = require('vs/base/common/winjs.base');
import objects = require('vs/base/common/objects');
import URI from 'vs/base/common/uri';
import EditorCommon = require('vs/editor/common/editorCommon');
import Modes = require('vs/editor/common/modes');
import {OneWorkerAttr} from 'vs/platform/thread/common/threadService';
import cssWorker = require('vs/languages/css/common/cssWorker');
import cssTokenTypes = require('vs/languages/css/common/cssTokenTypes');
import {AbstractMode} from 'vs/editor/common/modes/abstractMode';
import {AbstractState} from 'vs/editor/common/modes/abstractState';
import {AsyncDescriptor2, createAsyncDescriptor2} from 'vs/platform/instantiation/common/descriptors';
import {IMarker} from 'vs/platform/markers/common/markers';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {IThreadService} from 'vs/platform/thread/common/thread';
import {RichEditSupport} from 'vs/editor/common/modes/supports/richEditSupport';
import {TokenizationSupport} from 'vs/editor/common/modes/supports/tokenizationSupport';
import {DeclarationSupport} from 'vs/editor/common/modes/supports/declarationSupport';
import {ReferenceSupport} from 'vs/editor/common/modes/supports/referenceSupport';
import {SuggestSupport} from 'vs/editor/common/modes/supports/suggestSupport';
export enum States {
Selector,
Rule,
Value,
ValuePostUrl,
ValueInUrlFunction,
Unit,
Meta,
MetaPostUrl,
MetaInUrlFunction,
}
export { cssTokenTypes };
var identRegEx = /^-?-?([a-zA-Z]|(\\(([0-9a-fA-F]{1,6}\s?)|[^[0-9a-fA-F])))([\w\-]|(\\(([0-9a-fA-F]{1,6}\s?)|[^[0-9a-fA-F])))*/;
export class State extends AbstractState {
public kind:States;
public inComment:boolean;
public quote:string;
public inMeta:boolean;
public metaBraceCount: number;
constructor(mode:Modes.IMode, kind:States, inComment:boolean, quote:string, inMeta:boolean, metaBraceCount: number) {
super(mode);
this.kind = kind;
this.inComment = inComment;
this.quote = quote;
this.inMeta = inMeta;
this.metaBraceCount = metaBraceCount;
}
private nextState(next:States, token:Modes.ITokenizationResult):Modes.ITokenizationResult {
this.kind = next;
return token;
}
public makeClone():State {
return new State(this.getMode(), this.kind, this.inComment, this.quote, this.inMeta, this.metaBraceCount);
}
public equals(other:Modes.IState):boolean {
return super.equals(other) && objects.equals(this, other);
}
private tokenizeInComment(stream:Modes.IStream):Modes.ITokenizationResult {
if (/\*\/$/.test(stream.advanceUntilString('*/', true))) {
this.inComment = false;
}
return { type:'comment.css' };
}
private tokenizeInString(stream:Modes.IStream):Modes.ITokenizationResult {
var ch:string, afterBackslash = false, quote = this.quote;
while (!stream.eos()) {
ch = stream.next();
if (afterBackslash) {
// Ignore any character after \
afterBackslash = false;
} else if (ch === '\\') {
// Mark next character for ignoring
afterBackslash = true;
} else if (ch === quote) {
// Matching quote found
this.quote = null;
break;
}
}
return { type:'string.css' };
}
private consumeIdent(stream:Modes.IStream) {
stream.goBack(1);
if (stream.advanceIfRegExp2(identRegEx)) {
return true;
}
stream.advance(1);
return false;
}
public tokenize(stream:Modes.IStream):Modes.ITokenizationResult {
if (this.inComment) {
return this.tokenizeInComment(stream);
}
if (this.quote !== null) {
return this.tokenizeInString(stream);
}
if (stream.skipWhitespace2()) {
return { type:'' };
}
if (stream.advanceIfString2('/*')) {
this.inComment = true;
return this.tokenizeInComment(stream);
}
if (stream.advanceIfString2('\'')) {
this.quote = '\'';
return this.tokenizeInString(stream);
}
if (stream.advanceIfString2('\"')) {
this.quote = '\"';
return this.tokenizeInString(stream);
}
var ch = stream.next();
// These states can immediately transition to States.Value or Meta (without consuming ch), that's why they're handled above the switch stmt.
switch (this.kind) {
case States.ValuePostUrl:
if (ch === '(') {
return this.nextState(States.ValueInUrlFunction, { type: 'punctuation.parenthesis.css', bracket: Modes.Bracket.Open });
}
this.kind = States.Value;
break;
case States.MetaPostUrl:
if (ch === '(') {
return this.nextState(States.MetaInUrlFunction, { type: 'punctuation.parenthesis.css', bracket: Modes.Bracket.Open });
}
this.kind = States.Meta;
break;
case States.ValueInUrlFunction:
case States.MetaInUrlFunction:
// This state is after 'url(' was encountered in the value
if (ch !== ')') {
stream.advanceIfRegExp2(/^[^\)]*/);
return { type: 'string.css' };
}
this.kind = (this.kind === States.ValueInUrlFunction) ? States.Value : States.Meta;
break;
}
switch (this.kind) {
case States.Selector:
if (ch === '{') {
return this.nextState(States.Rule, { type: 'punctuation.bracket.css', bracket: Modes.Bracket.Open });
}
if (ch === '(' || ch === ')') {
return { type: 'punctuation.parenthesis.css', bracket: ch === '(' ? Modes.Bracket.Open : Modes.Bracket.Close };
}
if (ch === '@' && !this.inMeta) { //@import, @media, @key-word-animation
stream.advanceIfRegExp2(identRegEx);
return this.nextState(States.Meta, { type: cssTokenTypes.TOKEN_AT_KEYWORD + '.css' });
}
if (ch === '}' && this.inMeta) { //@import, @media, @key-word-animation
this.inMeta = false;
return this.nextState(States.Selector, { type: 'punctuation.bracket.css', bracket: Modes.Bracket.Close });
}
if (/[\*\(\)\[\]\+>=\~\|;]/.test(ch)) {
return { type: 'punctuation.css' };
}
if (ch === '#') {
stream.advanceIfRegExp2(identRegEx);
return { type: cssTokenTypes.TOKEN_SELECTOR + '.id.css' };
}
if (ch === '.') {
stream.advanceIfRegExp2(identRegEx);
return { type: cssTokenTypes.TOKEN_SELECTOR + '.class.css' };
}
this.consumeIdent(stream);
return { type: cssTokenTypes.TOKEN_SELECTOR_TAG + '.css' };
case States.Meta:
if (ch === '{') {
var nextState = States.Rule;
if (this.inMeta) {
nextState = States.Selector;
}
return this.nextState(nextState, { type: 'punctuation.bracket.css', bracket: Modes.Bracket.Open });
}
if (ch === '(' || ch === ')') {
return { type: 'punctuation.parenthesis.css', bracket: ch === '(' ? Modes.Bracket.Open : Modes.Bracket.Close };
}
if (ch === ';') {
if (this.metaBraceCount === 0) {
this.inMeta = false;
}
return this.nextState(States.Selector, { type: 'punctuation.css' });
}
if ((ch === 'u' || ch === 'U') && stream.advanceIfStringCaseInsensitive2('rl')) {
stream.advanceIfStringCaseInsensitive2('-prefix'); // support 'url-prefix' (part of @-mox-document)
return this.nextState(States.MetaPostUrl, { type: cssTokenTypes.TOKEN_VALUE + '.css' });
}
if (/[\*\(\)\[\]\+>=\~\|]/.test(ch)) {
return { type: 'punctuation.css' };
}
this.inMeta = true;
this.consumeIdent(stream);
return { type: cssTokenTypes.TOKEN_VALUE + '.css' };
case States.Rule:
if (ch === '}') {
return this.nextState(States.Selector, { type: 'punctuation.bracket.css', bracket: Modes.Bracket.Close });
}
if (ch === ':') {
return this.nextState(States.Value, { type: 'punctuation.css' });
}
if (ch === '(' || ch === ')') {
return { type: 'punctuation.parenthesis.css', bracket: ch === '(' ? Modes.Bracket.Open : Modes.Bracket.Close };
}
this.consumeIdent(stream);
return { type: cssTokenTypes.TOKEN_PROPERTY + '.css' };
case States.Value:
if (ch === '}') {
return this.nextState(States.Selector, { type: 'punctuation.bracket.css', bracket: Modes.Bracket.Close });
}
if (ch === ';') {
return this.nextState(States.Rule, { type: 'punctuation.css' });
}
if ((ch === 'u' || ch === 'U') && stream.advanceIfStringCaseInsensitive2('rl')) {
return this.nextState(States.ValuePostUrl, { type: cssTokenTypes.TOKEN_VALUE + '.css' });
}
if (ch === '(' || ch === ')') {
return { type: 'punctuation.parenthesis.css', bracket: ch === '(' ? Modes.Bracket.Open : Modes.Bracket.Close };
}
if (ch === ',') {
return { type: 'punctuation.css' };
}
if (ch === '#') {
stream.advanceIfRegExp2(/^[\w]*/);
return { type: cssTokenTypes.TOKEN_VALUE + '.hex.css' };
}
if (/\d/.test(ch) || (/-|\+/.test(ch) && !stream.eos() && /\d/.test(stream.peek()))) {
stream.advanceIfRegExp2(/^[\d\.]*/);
return this.nextState(States.Unit, { type: cssTokenTypes.TOKEN_VALUE + '.numeric.css' });
}
if (ch === '!') {
return { type: cssTokenTypes.TOKEN_VALUE + '.keyword.css' }; // !
}
if ((ch === 'i' || ch === 'I') && stream.advanceIfStringCaseInsensitive2('mportant')) {
return { type: cssTokenTypes.TOKEN_VALUE + '.keyword.css' }; // important
}
if (this.consumeIdent(stream)) {
return { type: cssTokenTypes.TOKEN_VALUE + '.css' };
}
break;
case States.Unit:
// css units - see: http://www.w3.org/TR/css3-values/#font-relative-lengths
stream.goBack(1);
if(stream.advanceIfRegExp2(/^(em|ex|ch|rem|vw|vh|vm|cm|mm|in|px|pt|pc|deg|grad|rad|turn|s|ms|Hz|kHz|%)/)) {
return { type: cssTokenTypes.TOKEN_VALUE + '.unit.css' };
}
// no unit, back to value state
this.nextState(States.Value, null);
return this.tokenize(stream);
}
return { type:'' };
}
}
export class CSSMode extends AbstractMode<cssWorker.CSSWorker> {
public tokenizationSupport: Modes.ITokenizationSupport;
public richEditSupport: Modes.IRichEditSupport;
public inplaceReplaceSupport:Modes.IInplaceReplaceSupport;
public referenceSupport: Modes.IReferenceSupport;
public logicalSelectionSupport: Modes.ILogicalSelectionSupport;
public extraInfoSupport:Modes.IExtraInfoSupport;
public occurrencesSupport:Modes.IOccurrencesSupport;
public outlineSupport: Modes.IOutlineSupport;
public declarationSupport: Modes.IDeclarationSupport;
public suggestSupport: Modes.ISuggestSupport;
public quickFixSupport: Modes.IQuickFixSupport;
constructor(
descriptor:Modes.IModeDescriptor,
@IInstantiationService instantiationService: IInstantiationService,
@IThreadService threadService: IThreadService
) {
super(descriptor, instantiationService, threadService);
this.tokenizationSupport = new TokenizationSupport(this, {
getInitialState: () => new State(this, States.Selector, false, null, false, 0)
}, false, false);
this.richEditSupport = new RichEditSupport(this.getId(), {
// TODO@Martin: This definition does not work with umlauts for example
wordPattern: /(#?-?\d*\.\d\w*%?)|((::|[@#.!:])?[\w-?]+%?)|::|[@#.!:]/g,
comments: {
blockComment: ['/*', '*/']
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')']
],
__electricCharacterSupport: {
brackets: [
{ tokenType: 'punctuation.bracket.css', open: '{', close: '}', isElectric: true }
]
},
__characterPairSupport: {
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"', notIn: ['string'] },
{ open: '\'', close: '\'', notIn: ['string'] }
]
}
});
this.inplaceReplaceSupport = this;
this.occurrencesSupport = this;
this.extraInfoSupport = this;
this.referenceSupport = new ReferenceSupport(this.getId(), {
tokens: [cssTokenTypes.TOKEN_PROPERTY + '.css', cssTokenTypes.TOKEN_VALUE + '.css', cssTokenTypes.TOKEN_SELECTOR_TAG + '.css'],
findReferences: (resource, position, /*unused*/includeDeclaration) => this.findReferences(resource, position)});
this.logicalSelectionSupport = this;
this.outlineSupport = this;
this.declarationSupport = new DeclarationSupport(this.getId(), {
tokens: [cssTokenTypes.TOKEN_VALUE + '.css'],
findDeclaration: (resource, position) => this.findDeclaration(resource, position)});
this.suggestSupport = new SuggestSupport(this.getId(), {
triggerCharacters: [' ', ':'],
excludeTokens: ['comment.css', 'string.css'],
suggest: (resource, position) => this.suggest(resource, position)});
this.quickFixSupport = this;
}
protected _getWorkerDescriptor(): AsyncDescriptor2<Modes.IMode, Modes.IWorkerParticipant[], cssWorker.CSSWorker> {
return createAsyncDescriptor2('vs/languages/css/common/cssWorker', 'CSSWorker');
}
static $navigateValueSet = OneWorkerAttr(CSSMode, CSSMode.prototype.navigateValueSet);
public navigateValueSet(resource:URI, position:EditorCommon.IRange, up:boolean):WinJS.TPromise<Modes.IInplaceReplaceSupportResult> {
return this._worker((w) => w.navigateValueSet(resource, position, up));
}
static $findOccurrences = OneWorkerAttr(CSSMode, CSSMode.prototype.findOccurrences);
public findOccurrences(resource:URI, position:EditorCommon.IPosition, strict:boolean = false): WinJS.TPromise<Modes.IOccurence[]> {
return this._worker((w) => w.findOccurrences(resource, position, strict));
}
static $suggest = OneWorkerAttr(CSSMode, CSSMode.prototype.suggest);
public suggest(resource:URI, position:EditorCommon.IPosition):WinJS.TPromise<Modes.ISuggestResult[]> {
return this._worker((w) => w.suggest(resource, position));
}
static $findDeclaration = OneWorkerAttr(CSSMode, CSSMode.prototype.findDeclaration);
public findDeclaration(resource:URI, position:EditorCommon.IPosition):WinJS.TPromise<Modes.IReference> {
return this._worker((w) => w.findDeclaration(resource, position));
}
static $computeInfo = OneWorkerAttr(CSSMode, CSSMode.prototype.computeInfo);
public computeInfo(resource:URI, position:EditorCommon.IPosition): WinJS.TPromise<Modes.IComputeExtraInfoResult> {
return this._worker((w) => w.computeInfo(resource, position));
}
static $findReferences = OneWorkerAttr(CSSMode, CSSMode.prototype.findReferences);
public findReferences(resource:URI, position:EditorCommon.IPosition):WinJS.TPromise<Modes.IReference[]> {
return this._worker((w) => w.findReferences(resource, position));
}
static $getRangesToPosition = OneWorkerAttr(CSSMode, CSSMode.prototype.getRangesToPosition);
public getRangesToPosition(resource:URI, position:EditorCommon.IPosition):WinJS.TPromise<Modes.ILogicalSelectionEntry[]> {
return this._worker((w) => w.getRangesToPosition(resource, position));
}
static $getOutline = OneWorkerAttr(CSSMode, CSSMode.prototype.getOutline);
public getOutline(resource:URI):WinJS.TPromise<Modes.IOutlineEntry[]> {
return this._worker((w) => w.getOutline(resource));
}
static $findColorDeclarations = OneWorkerAttr(CSSMode, CSSMode.prototype.findColorDeclarations);
public findColorDeclarations(resource:URI):WinJS.TPromise<{range:EditorCommon.IRange; value:string; }[]> {
return this._worker((w) => w.findColorDeclarations(resource));
}
static getQuickFixes = OneWorkerAttr(CSSMode, CSSMode.prototype.getQuickFixes);
public getQuickFixes(resource: URI, marker: IMarker | EditorCommon.IRange): WinJS.TPromise<Modes.IQuickFix[]>{
return this._worker((w) => w.getQuickFixes(resource, marker));
}
static runQuickFixAction = OneWorkerAttr(CSSMode, CSSMode.prototype.runQuickFixAction);
public runQuickFixAction(resource:URI, range:EditorCommon.IRange, id: any):WinJS.TPromise<Modes.IQuickFixResult>{
return this._worker((w) => w.runQuickFixAction(resource, range, id));
}
}