Skip to content

Commit

Permalink
Allow to run css language server headless
Browse files Browse the repository at this point in the history
  • Loading branch information
aeschli committed May 22, 2020
1 parent 452dc54 commit 2e5b082
Show file tree
Hide file tree
Showing 22 changed files with 1,097 additions and 718 deletions.
17 changes: 16 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@
{
"type": "node",
"request": "launch",
"name": "HTML Unit Tests",
"name": "HTML Server Unit Tests",
"program": "${workspaceFolder}/extensions/html-language-features/server/test/index.js",
"stopOnEntry": false,
"cwd": "${workspaceFolder}/extensions/html-language-features/server",
Expand All @@ -286,6 +286,21 @@
"order": 10
}
},
{
"type": "node",
"request": "launch",
"name": "CSS Server Unit Tests",
"program": "${workspaceFolder}/extensions/css-language-features/server/test/index.js",
"stopOnEntry": false,
"cwd": "${workspaceFolder}/extensions/css-language-features/server",
"outFiles": [
"${workspaceFolder}/extensions/css-language-features/server/out/**/*.js"
],
"presentation": {
"group": "5_tests",
"order": 10
}
},
{
"type": "extensionHost",
"request": "launch",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,29 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as fs from 'fs';
import * as path from 'path';
import { commands, CompletionItem, CompletionItemKind, ExtensionContext, languages, Position, Range, SnippetString, TextEdit, window, workspace, TextDocument, CompletionContext, CancellationToken, ProviderResult, CompletionList } from 'vscode';
import { Disposable, LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, ProvideCompletionItemsSignature } from 'vscode-languageclient';
import { commands, CompletionItem, CompletionItemKind, ExtensionContext, languages, Position, Range, SnippetString, TextEdit, window, TextDocument, CompletionContext, CancellationToken, ProviderResult, CompletionList, extensions } from 'vscode';
import { Disposable, LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, ProvideCompletionItemsSignature, NotificationType } from 'vscode-languageclient';
import * as nls from 'vscode-nls';
import { getCustomDataPathsFromAllExtensions, getCustomDataPathsInAllWorkspaces } from './customData';
import { getCustomDataSource } from './customData';
import { RequestService, serveFileSystemRequests } from './requests';

namespace CustomDataChangedNotification {
export const type: NotificationType<string[]> = new NotificationType('css/customDataChanged');
}

const localize = nls.loadMessageBundle();

// this method is called when vs code is activated
export function activate(context: ExtensionContext) {
export function startClient(context: ExtensionContext, runtime: { fs?: RequestService }) {

const clientMain = extensions.getExtension('vscode.css-language-features')?.packageJSON?.main;
const serverMain = clientMain?.replace('client', 'server').replace('cssClientMain', 'cssServerMain');
if (!serverMain) {
throw new Error('Unable to compute CSS server module path. Client: ' + clientMain);
}

const serverModule = context.asAbsolutePath(serverMain);

let serverMain = readJSONFile(context.asAbsolutePath('./server/package.json')).main;
let serverModule = context.asAbsolutePath(path.join('server', serverMain));
const customDataSource = getCustomDataSource(context.subscriptions);

// The debug options for the server
let debugOptions = { execArgv: ['--nolazy', '--inspect=6044'] };
Expand All @@ -30,19 +39,14 @@ export function activate(context: ExtensionContext) {

let documentSelector = ['css', 'scss', 'less'];

let dataPaths = [
...getCustomDataPathsInAllWorkspaces(workspace.workspaceFolders),
...getCustomDataPathsFromAllExtensions()
];

// Options to control the language client
let clientOptions: LanguageClientOptions = {
documentSelector,
synchronize: {
configurationSection: ['css', 'scss', 'less']
},
initializationOptions: {
dataPaths
handledSchemas: ['file']
},
middleware: {
provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult<CompletionItem[] | CompletionList> {
Expand Down Expand Up @@ -84,6 +88,15 @@ export function activate(context: ExtensionContext) {
// Create the language client and start the client.
let client = new LanguageClient('css', localize('cssserver.name', 'CSS Language Server'), serverOptions, clientOptions);
client.registerProposedFeatures();
client.onReady().then(() => {

client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
customDataSource.onDidChange(() => {
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
});

serveFileSystemRequests(client, runtime);
});

let disposable = client.start();
// Push the disposable to the context's subscriptions so that the
Expand Down Expand Up @@ -162,13 +175,3 @@ export function activate(context: ExtensionContext) {
}
}
}

function readJSONFile(location: string) {
try {
return JSON.parse(fs.readFileSync(location).toString());
} catch (e) {
console.log(`Problems reading ${location}: ${e}`);
return {};
}
}

78 changes: 57 additions & 21 deletions extensions/css-language-features/client/src/customData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,78 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as path from 'path';
import { workspace, WorkspaceFolder, extensions } from 'vscode';
import { workspace, extensions, Uri, EventEmitter, Disposable } from 'vscode';
import { resolvePath } from './requests';

interface ExperimentalConfig {
customData?: string[];
experimental?: {
customData?: string[];
export function getCustomDataSource(toDispose: Disposable[]) {
let pathsInWorkspace = getCustomDataPathsInAllWorkspaces();
let pathsInExtensions = getCustomDataPathsFromAllExtensions();

const onChange = new EventEmitter<void>();

toDispose.push(extensions.onDidChange(_ => {
const newPathsInExtensions = getCustomDataPathsFromAllExtensions();
if (newPathsInExtensions.length !== pathsInExtensions.length || !newPathsInExtensions.every((val, idx) => val === pathsInExtensions[idx])) {
pathsInExtensions = newPathsInExtensions;
onChange.fire();
}
}));
toDispose.push(workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('css.customData')) {
pathsInWorkspace = getCustomDataPathsInAllWorkspaces();
onChange.fire();
}
}));

return {
get uris() {
return pathsInWorkspace.concat(pathsInExtensions);
},
get onDidChange() {
return onChange.event;
}
};
}

export function getCustomDataPathsInAllWorkspaces(workspaceFolders: readonly WorkspaceFolder[] | undefined): string[] {

function getCustomDataPathsInAllWorkspaces(): string[] {
const workspaceFolders = workspace.workspaceFolders;

const dataPaths: string[] = [];

if (!workspaceFolders) {
return dataPaths;
}

workspaceFolders.forEach(wf => {
const allCssConfig = workspace.getConfiguration(undefined, wf.uri);
const wfCSSConfig = allCssConfig.inspect<ExperimentalConfig>('css');
if (wfCSSConfig && wfCSSConfig.workspaceFolderValue && wfCSSConfig.workspaceFolderValue.customData) {
const customData = wfCSSConfig.workspaceFolderValue.customData;
if (Array.isArray(customData)) {
customData.forEach(t => {
if (typeof t === 'string') {
dataPaths.push(path.resolve(wf.uri.fsPath, t));
}
});
const collect = (paths: string[] | undefined, rootFolder: Uri) => {
if (Array.isArray(paths)) {
for (const path of paths) {
if (typeof path === 'string') {
dataPaths.push(resolvePath(rootFolder, path).toString());
}
}
}
};

for (let i = 0; i < workspaceFolders.length; i++) {
const folderUri = workspaceFolders[i].uri;
const allCssConfig = workspace.getConfiguration('css', folderUri);
const customDataInspect = allCssConfig.inspect<string[]>('customData');
if (customDataInspect) {
collect(customDataInspect.workspaceFolderValue, folderUri);
if (i === 0) {
if (workspace.workspaceFile) {
collect(customDataInspect.workspaceValue, workspace.workspaceFile);
}
collect(customDataInspect.globalValue, folderUri);
}
}
});

}
return dataPaths;
}

export function getCustomDataPathsFromAllExtensions(): string[] {
function getCustomDataPathsFromAllExtensions(): string[] {
const dataPaths: string[] = [];

for (const extension of extensions.all) {
Expand All @@ -47,7 +83,7 @@ export function getCustomDataPathsFromAllExtensions(): string[] {
if (contributes && contributes.css && contributes.css.customData && Array.isArray(contributes.css.customData)) {
const relativePaths: string[] = contributes.css.customData;
relativePaths.forEach(rp => {
dataPaths.push(path.resolve(extension.extensionPath, rp));
dataPaths.push(Uri.joinPath(extension.extensionUri, rp).toString());
});
}
}
Expand Down
13 changes: 13 additions & 0 deletions extensions/css-language-features/client/src/node/cssClientMain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { getNodeFSRequestService } from './nodeFs';
import { ExtensionContext } from 'vscode';
import { startClient } from '../cssClient';

// this method is called when vs code is activated
export function activate(context: ExtensionContext) {
startClient(context, { fs: getNodeFSRequestService() });
}
85 changes: 85 additions & 0 deletions extensions/css-language-features/client/src/node/nodeFs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as fs from 'fs';
import { Uri } from 'vscode';
import { getScheme, RequestService, FileType } from '../requests';

export function getNodeFSRequestService(): RequestService {
function ensureFileUri(location: string) {
if (getScheme(location) !== 'file') {
throw new Error('fileRequestService can only handle file URLs');
}
}
return {
getContent(location: string, encoding?: string) {
ensureFileUri(location);
return new Promise((c, e) => {
const uri = Uri.parse(location);
fs.readFile(uri.fsPath, encoding, (err, buf) => {
if (err) {
return e(err);
}
c(buf.toString());

});
});
},
stat(location: string) {
ensureFileUri(location);
return new Promise((c, e) => {
const uri = Uri.parse(location);
fs.stat(uri.fsPath, (err, stats) => {
if (err) {
if (err.code === 'ENOENT') {
return c({ type: FileType.Unknown, ctime: -1, mtime: -1, size: -1 });
} else {
return e(err);
}
}

let type = FileType.Unknown;
if (stats.isFile()) {
type = FileType.File;
} else if (stats.isDirectory()) {
type = FileType.Directory;
} else if (stats.isSymbolicLink()) {
type = FileType.SymbolicLink;
}

c({
type,
ctime: stats.ctime.getTime(),
mtime: stats.mtime.getTime(),
size: stats.size
});
});
});
},
readDirectory(location: string) {
ensureFileUri(location);
return new Promise((c, e) => {
const path = Uri.parse(location).fsPath;

fs.readdir(path, { withFileTypes: true }, (err, children) => {
if (err) {
return e(err);
}
c(children.map(stat => {
if (stat.isSymbolicLink()) {
return [stat.name, FileType.SymbolicLink];
} else if (stat.isDirectory()) {
return [stat.name, FileType.Directory];
} else if (stat.isFile()) {
return [stat.name, FileType.File];
} else {
return [stat.name, FileType.Unknown];
}
}));
});
});
}
};
}
Loading

0 comments on commit 2e5b082

Please sign in to comment.