Skip to content

Commit

Permalink
(feat) css path completion (#1533)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonlyu123 committed Jun 20, 2022
1 parent 09cd61c commit c44706a
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 68 deletions.
8 changes: 5 additions & 3 deletions packages/language-server/src/plugins/css/CSSDocument.ts
@@ -1,7 +1,7 @@
import { Stylesheet, TextDocument } from 'vscode-css-languageservice';
import { Position } from 'vscode-languageserver';
import { getLanguageService } from './service';
import { Document, DocumentMapper, ReadableDocument, TagInformation } from '../../lib/documents';
import { CSSLanguageServices, getLanguageService } from './service';

export interface CSSDocumentBase extends DocumentMapper, TextDocument {
languageId: string;
Expand All @@ -15,7 +15,7 @@ export class CSSDocument extends ReadableDocument implements DocumentMapper {
public stylesheet: Stylesheet;
public languageId: string;

constructor(private parent: Document) {
constructor(private parent: Document, languageServices: CSSLanguageServices) {
super();

if (this.parent.styleInfo) {
Expand All @@ -29,7 +29,9 @@ export class CSSDocument extends ReadableDocument implements DocumentMapper {
}

this.languageId = this.language;
this.stylesheet = getLanguageService(this.language).parseStylesheet(this);
this.stylesheet = getLanguageService(languageServices, this.languageId).parseStylesheet(
this
);
}

/**
Expand Down
63 changes: 41 additions & 22 deletions packages/language-server/src/plugins/css/CSSPlugin.ts
Expand Up @@ -13,7 +13,8 @@ import {
SymbolInformation,
CompletionItem,
CompletionItemKind,
SelectionRange
SelectionRange,
WorkspaceFolder
} from 'vscode-languageserver';
import {
Document,
Expand All @@ -38,11 +39,12 @@ import {
SelectionRangeProvider
} from '../interfaces';
import { CSSDocument, CSSDocumentBase } from './CSSDocument';
import { getLanguage, getLanguageService } from './service';
import { CSSLanguageServices, getLanguage, getLanguageService } from './service';
import { GlobalVars } from './global-vars';
import { getIdClassCompletion } from './features/getIdClassCompletion';
import { AttributeContext, getAttributeContextAtPosition } from '../../lib/documents/parseHtml';
import { StyleAttributeDocument } from './StyleAttributeDocument';
import { getDocumentContext } from '../documentContext';

export class CSSPlugin
implements
Expand All @@ -57,10 +59,19 @@ export class CSSPlugin
__name = 'css';
private configManager: LSConfigManager;
private cssDocuments = new WeakMap<Document, CSSDocument>();
private cssLanguageServices: CSSLanguageServices;
private workspaceFolders: WorkspaceFolder[];
private triggerCharacters = ['.', ':', '-', '/'];
private globalVars = new GlobalVars();

constructor(docManager: DocumentManager, configManager: LSConfigManager) {
constructor(
docManager: DocumentManager,
configManager: LSConfigManager,
workspaceFolders: WorkspaceFolder[],
cssLanguageServices: CSSLanguageServices
) {
this.cssLanguageServices = cssLanguageServices;
this.workspaceFolders = workspaceFolders;
this.configManager = configManager;
this.updateConfigs();

Expand All @@ -71,7 +82,7 @@ export class CSSPlugin
});

docManager.on('documentChange', (document) =>
this.cssDocuments.set(document, new CSSDocument(document))
this.cssDocuments.set(document, new CSSDocument(document, this.cssLanguageServices))
);
docManager.on('documentClose', (document) => this.cssDocuments.delete(document));
}
Expand All @@ -82,7 +93,7 @@ export class CSSPlugin
}

const cssDocument = this.getCSSDoc(document);
const [range] = getLanguageService(extractLanguage(cssDocument)).getSelectionRanges(
const [range] = this.getLanguageService(extractLanguage(cssDocument)).getSelectionRanges(
cssDocument,
[cssDocument.getGeneratedPosition(position)],
cssDocument.stylesheet
Expand All @@ -107,7 +118,7 @@ export class CSSPlugin
return [];
}

return getLanguageService(kind)
return this.getLanguageService(kind)
.doValidation(cssDocument, cssDocument.stylesheet)
.map((diagnostic) => ({ ...diagnostic, source: getLanguage(kind) }))
.map((diagnostic) => mapObjWithRangeToOriginal(cssDocument, diagnostic));
Expand All @@ -131,25 +142,28 @@ export class CSSPlugin
this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())
) {
const [start, end] = attributeContext.valueRange;
return this.doHoverInternal(new StyleAttributeDocument(document, start, end), position);
return this.doHoverInternal(
new StyleAttributeDocument(document, start, end, this.cssLanguageServices),
position
);
}

return null;
}
private doHoverInternal(cssDocument: CSSDocumentBase, position: Position) {
const hoverInfo = getLanguageService(extractLanguage(cssDocument)).doHover(
const hoverInfo = this.getLanguageService(extractLanguage(cssDocument)).doHover(
cssDocument,
cssDocument.getGeneratedPosition(position),
cssDocument.stylesheet
);
return hoverInfo ? mapHoverToParent(cssDocument, hoverInfo) : hoverInfo;
}

getCompletions(
async getCompletions(
document: Document,
position: Position,
completionContext?: CompletionContext
): CompletionList | null {
): Promise<CompletionList | null> {
const triggerCharacter = completionContext?.triggerCharacter;
const triggerKind = completionContext?.triggerKind;
const isCustomTriggerCharacter = triggerKind === CompletionTriggerKind.TriggerCharacter;
Expand Down Expand Up @@ -182,7 +196,7 @@ export class CSSPlugin
return this.getCompletionsInternal(
document,
position,
new StyleAttributeDocument(document, start, end)
new StyleAttributeDocument(document, start, end, this.cssLanguageServices)
);
} else {
return getIdClassCompletion(cssDocument, attributeContext);
Expand All @@ -200,7 +214,7 @@ export class CSSPlugin
);
}

private getCompletionsInternal(
private async getCompletionsInternal(
document: Document,
position: Position,
cssDocument: CSSDocumentBase
Expand All @@ -219,7 +233,7 @@ export class CSSPlugin
return null;
}

const lang = getLanguageService(type);
const lang = this.getLanguageService(type);
let emmetResults: CompletionList = {
isIncomplete: false,
items: []
Expand Down Expand Up @@ -256,10 +270,11 @@ export class CSSPlugin
]);
}

const results = lang.doComplete(
const results = await lang.doComplete2(
cssDocument,
cssDocument.getGeneratedPosition(position),
cssDocument.stylesheet
cssDocument.stylesheet,
getDocumentContext(cssDocument.uri, this.workspaceFolders)
);
return CompletionList.create(
this.appendGlobalVars(
Expand Down Expand Up @@ -301,7 +316,7 @@ export class CSSPlugin
return [];
}

return getLanguageService(extractLanguage(cssDocument))
return this.getLanguageService(extractLanguage(cssDocument))
.findDocumentColors(cssDocument, cssDocument.stylesheet)
.map((colorInfo) => mapObjWithRangeToOriginal(cssDocument, colorInfo));
}
Expand All @@ -319,7 +334,7 @@ export class CSSPlugin
return [];
}

return getLanguageService(extractLanguage(cssDocument))
return this.getLanguageService(extractLanguage(cssDocument))
.getColorPresentations(
cssDocument,
cssDocument.stylesheet,
Expand All @@ -340,7 +355,7 @@ export class CSSPlugin
return [];
}

return getLanguageService(extractLanguage(cssDocument))
return this.getLanguageService(extractLanguage(cssDocument))
.findDocumentSymbols(cssDocument, cssDocument.stylesheet)
.map((symbol) => {
if (!symbol.containerName) {
Expand All @@ -359,16 +374,16 @@ export class CSSPlugin
private getCSSDoc(document: Document) {
let cssDoc = this.cssDocuments.get(document);
if (!cssDoc || cssDoc.version < document.version) {
cssDoc = new CSSDocument(document);
cssDoc = new CSSDocument(document, this.cssLanguageServices);
this.cssDocuments.set(document, cssDoc);
}
return cssDoc;
}

private updateConfigs() {
getLanguageService('css')?.configure(this.configManager.getCssConfig());
getLanguageService('scss')?.configure(this.configManager.getScssConfig());
getLanguageService('less')?.configure(this.configManager.getLessConfig());
this.getLanguageService('css')?.configure(this.configManager.getCssConfig());
this.getLanguageService('scss')?.configure(this.configManager.getScssConfig());
this.getLanguageService('less')?.configure(this.configManager.getLessConfig());
}

private featureEnabled(feature: keyof LSCSSConfig) {
Expand All @@ -377,6 +392,10 @@ export class CSSPlugin
this.configManager.enabled(`css.${feature}.enable`)
);
}

private getLanguageService(kind: string) {
return getLanguageService(this.cssLanguageServices, kind);
}
}

function shouldExcludeValidation(kind?: string) {
Expand Down
94 changes: 94 additions & 0 deletions packages/language-server/src/plugins/css/FileSystemProvider.ts
@@ -0,0 +1,94 @@
import { stat, readdir, Stats } from 'fs';
import { promisify } from 'util';
import {
FileStat,
FileSystemProvider as CSSFileSystemProvider,
FileType
} from 'vscode-css-languageservice';
import { urlToPath } from '../../utils';

interface StatLike {
isDirectory(): boolean;
isFile(): boolean;
isSymbolicLink(): boolean;
}

export class FileSystemProvider implements CSSFileSystemProvider {
// TODO use fs/promises after we bumps the target nodejs versions
private promisifyStat = promisify(stat);
private promisifyReaddir = promisify(readdir);

constructor() {
this.readDirectory = this.readDirectory.bind(this);
this.stat = this.stat.bind(this);
}

async stat(uri: string): Promise<FileStat> {
const path = urlToPath(uri);

if (!path) {
return this.unknownStat();
}

let stat: Stats;
try {
stat = await this.promisifyStat(path);
} catch (error) {
if (
error != null &&
typeof error === 'object' &&
'code' in error &&
(error as { code: string }).code === 'ENOENT'
) {
return {
type: FileType.Unknown,
ctime: -1,
mtime: -1,
size: -1
};
}

throw error;
}

return {
ctime: stat.ctimeMs,
mtime: stat.mtimeMs,
size: stat.size,
type: this.getFileType(stat)
};
}

private unknownStat(): FileStat {
return {
type: FileType.Unknown,
ctime: -1,
mtime: -1,
size: -1
};
}

private getFileType(stat: StatLike) {
return stat.isDirectory()
? FileType.Directory
: stat.isFile()
? FileType.File
: stat.isSymbolicLink()
? FileType.SymbolicLink
: FileType.Unknown;
}

async readDirectory(uri: string): Promise<Array<[string, FileType]>> {
const path = urlToPath(uri);

if (!path) {
return [];
}

const files = await this.promisifyReaddir(path, {
withFileTypes: true
});

return files.map((file) => [file.name, this.getFileType(file)]);
}
}
@@ -1,6 +1,6 @@
import { Stylesheet } from 'vscode-css-languageservice';
import { Position } from 'vscode-languageserver';
import { getLanguageService } from './service';
import { CSSLanguageServices, getLanguageService } from './service';
import { Document, DocumentMapper, ReadableDocument } from '../../lib/documents';

const PREFIX = '__ {';
Expand All @@ -15,11 +15,12 @@ export class StyleAttributeDocument extends ReadableDocument implements Document
constructor(
private readonly parent: Document,
private readonly attrStart: number,
private readonly attrEnd: number
private readonly attrEnd: number,
languageServices: CSSLanguageServices
) {
super();

this.stylesheet = getLanguageService(this.languageId).parseStylesheet(this);
this.stylesheet = getLanguageService(languageServices).parseStylesheet(this);
}

/**
Expand Down

0 comments on commit c44706a

Please sign in to comment.