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

(feat) css path completion #1533

Merged
merged 6 commits into from
Jun 20, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions packages/language-server/src/lib/FileSystemProvider.ts
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You added this to lib and it's only used inside plugins/css. What's the reason for this? Do you think this could be useful outside the CSS plugin, too, or that it's not directly connected to the CSS plugin?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vscode-html-languageservice has the same API for path completion. Originally I was thinking maybe we can add it too. But I am not sure is useful inside a svelte file. Since most of the time, the relative path to an asset does work like plain HTML. I think I'll move it to plugins/css for now. We could move it out if we want to use it in HTML later.

// 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lowercase first

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)]);
}
}
42 changes: 42 additions & 0 deletions packages/language-server/src/lib/documents/documentContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

// adopted from https://github.com/microsoft/vscode/blob/5ffcfde11d8b1b57634627f5094907789db09776/extensions/css-language-features/server/src/utils/documentContext.ts

import { DocumentContext } from 'vscode-css-languageservice';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question like in FileSystemProvider

import { WorkspaceFolder } from 'vscode-languageserver';
import { Utils, URI } from 'vscode-uri';

export function getDocumentContext(
documentUri: string,
workspaceFolders: WorkspaceFolder[]
): DocumentContext {
function getRootFolder(): string | undefined {
for (const folder of workspaceFolders) {
let folderURI = folder.uri;
if (!folderURI.endsWith('/')) {
folderURI = folderURI + '/';
}
if (documentUri.startsWith(folderURI)) {
return folderURI;
}
}
return undefined;
}

return {
resolveReference: (ref: string, base = documentUri) => {
if (ref[0] === '/') {
// resolve absolute path against the current workspace folder
const folderUri = getRootFolder();
if (folderUri) {
return folderUri + ref.substr(1);
}
}
base = base.substr(0, base.lastIndexOf('/') + 1);
return Utils.resolvePath(URI.parse(base), ref).toString();
}
};
}
8 changes: 5 additions & 3 deletions packages/language-server/src/plugins/css/CSSDocument.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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 '../../lib/documents/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
Original file line number Diff line number Diff line change
@@ -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
Loading