chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ExtensionContext, Uri, l10n } from 'vscode';
|
||||
import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient';
|
||||
import { startClient, LanguageClientConstructor } from '../cssClient';
|
||||
import { LanguageClient } from 'vscode-languageclient/browser';
|
||||
import { registerDropOrPasteResourceSupport } from '../dropOrPaste/dropOrPasteResource';
|
||||
|
||||
let client: BaseLanguageClient | undefined;
|
||||
|
||||
// this method is called when vs code is activated
|
||||
export async function activate(context: ExtensionContext) {
|
||||
const serverMain = Uri.joinPath(context.extensionUri, 'server/dist/browser/cssServerMain.js');
|
||||
try {
|
||||
const worker = new Worker(serverMain.toString());
|
||||
worker.postMessage({ i10lLocation: l10n.uri?.toString(false) ?? '' });
|
||||
|
||||
const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => {
|
||||
return new LanguageClient(id, name, worker, clientOptions);
|
||||
};
|
||||
|
||||
client = await startClient(context, newLanguageClient, { TextDecoder });
|
||||
|
||||
context.subscriptions.push(registerDropOrPasteResourceSupport({ language: 'css', scheme: '*' }));
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deactivate(): Promise<void> {
|
||||
if (client) {
|
||||
await client.stop();
|
||||
client = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { commands, CompletionItem, CompletionItemKind, ExtensionContext, languages, Position, Range, SnippetString, TextEdit, window, TextDocument, CompletionContext, CancellationToken, ProviderResult, CompletionList, FormattingOptions, workspace, l10n } from 'vscode';
|
||||
import { Disposable, LanguageClientOptions, ProvideCompletionItemsSignature, NotificationType, BaseLanguageClient, DocumentRangeFormattingParams, DocumentRangeFormattingRequest } from 'vscode-languageclient';
|
||||
import { getCustomDataSource } from './customData';
|
||||
import { RequestService, serveFileSystemRequests } from './requests';
|
||||
|
||||
namespace CustomDataChangedNotification {
|
||||
export const type: NotificationType<string[]> = new NotificationType('css/customDataChanged');
|
||||
}
|
||||
|
||||
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;
|
||||
|
||||
export interface Runtime {
|
||||
TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string } };
|
||||
fs?: RequestService;
|
||||
}
|
||||
|
||||
interface FormatterRegistration {
|
||||
readonly languageId: string;
|
||||
readonly settingId: string;
|
||||
provider: Disposable | undefined;
|
||||
}
|
||||
|
||||
interface CSSFormatSettings {
|
||||
newlineBetweenSelectors?: boolean;
|
||||
newlineBetweenRules?: boolean;
|
||||
spaceAroundSelectorSeparator?: boolean;
|
||||
braceStyle?: 'collapse' | 'expand';
|
||||
preserveNewLines?: boolean;
|
||||
maxPreserveNewLines?: number | null;
|
||||
}
|
||||
|
||||
const cssFormatSettingKeys: (keyof CSSFormatSettings)[] = ['newlineBetweenSelectors', 'newlineBetweenRules', 'spaceAroundSelectorSeparator', 'braceStyle', 'preserveNewLines', 'maxPreserveNewLines'];
|
||||
|
||||
export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<BaseLanguageClient> {
|
||||
|
||||
const customDataSource = getCustomDataSource(context.subscriptions);
|
||||
|
||||
const documentSelector = ['css', 'scss', 'less'];
|
||||
|
||||
const formatterRegistrations: FormatterRegistration[] = documentSelector.map(languageId => ({
|
||||
languageId, settingId: `${languageId}.format.enable`, provider: undefined
|
||||
}));
|
||||
|
||||
// Options to control the language client
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
documentSelector,
|
||||
synchronize: {
|
||||
configurationSection: ['css', 'scss', 'less']
|
||||
},
|
||||
initializationOptions: {
|
||||
handledSchemas: ['file'],
|
||||
provideFormatter: false, // tell the server to not provide formatting capability
|
||||
customCapabilities: { rangeFormatting: { editLimit: 10000 } }
|
||||
},
|
||||
middleware: {
|
||||
provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult<CompletionItem[] | CompletionList> {
|
||||
// testing the replace / insert mode
|
||||
function updateRanges(item: CompletionItem) {
|
||||
const range = item.range;
|
||||
if (range instanceof Range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) {
|
||||
item.range = { inserting: new Range(range.start, position), replacing: range };
|
||||
|
||||
}
|
||||
}
|
||||
function updateLabel(item: CompletionItem) {
|
||||
if (item.kind === CompletionItemKind.Color) {
|
||||
item.label = {
|
||||
label: item.label as string,
|
||||
description: (item.documentation as string)
|
||||
};
|
||||
}
|
||||
}
|
||||
// testing the new completion
|
||||
function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined {
|
||||
if (r) {
|
||||
(Array.isArray(r) ? r : r.items).forEach(updateRanges);
|
||||
(Array.isArray(r) ? r : r.items).forEach(updateLabel);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
const isThenable = <T>(obj: ProviderResult<T>): obj is Thenable<T> => obj && (<any>obj)['then'];
|
||||
|
||||
const r = next(document, position, context, token);
|
||||
if (isThenable<CompletionItem[] | CompletionList | null | undefined>(r)) {
|
||||
return r.then(updateProposals);
|
||||
}
|
||||
return updateProposals(r);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create the language client and start the client.
|
||||
const client = newLanguageClient('css', l10n.t('CSS Language Server'), clientOptions);
|
||||
client.registerProposedFeatures();
|
||||
|
||||
await client.start();
|
||||
|
||||
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
|
||||
customDataSource.onDidChange(() => {
|
||||
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
|
||||
});
|
||||
|
||||
// manually register / deregister format provider based on the `css/less/scss.format.enable` setting avoiding issues with late registration. See #71652.
|
||||
for (const registration of formatterRegistrations) {
|
||||
updateFormatterRegistration(registration);
|
||||
context.subscriptions.push({ dispose: () => registration.provider?.dispose() });
|
||||
context.subscriptions.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration(registration.settingId) && updateFormatterRegistration(registration)));
|
||||
}
|
||||
|
||||
serveFileSystemRequests(client, runtime);
|
||||
|
||||
|
||||
context.subscriptions.push(initCompletionProvider());
|
||||
|
||||
function initCompletionProvider(): Disposable {
|
||||
const regionCompletionRegExpr = /^(\s*)(\/(\*\s*(#\w*)?)?)?$/;
|
||||
|
||||
return languages.registerCompletionItemProvider(documentSelector, {
|
||||
provideCompletionItems(doc: TextDocument, pos: Position) {
|
||||
const lineUntilPos = doc.getText(new Range(new Position(pos.line, 0), pos));
|
||||
const match = lineUntilPos.match(regionCompletionRegExpr);
|
||||
if (match) {
|
||||
const range = new Range(new Position(pos.line, match[1].length), pos);
|
||||
const beginProposal = new CompletionItem('#region', CompletionItemKind.Snippet);
|
||||
beginProposal.range = range; TextEdit.replace(range, '/* #region */');
|
||||
beginProposal.insertText = new SnippetString('/* #region $1*/');
|
||||
beginProposal.documentation = l10n.t('Folding Region Start');
|
||||
beginProposal.filterText = match[2];
|
||||
beginProposal.sortText = 'za';
|
||||
const endProposal = new CompletionItem('#endregion', CompletionItemKind.Snippet);
|
||||
endProposal.range = range;
|
||||
endProposal.insertText = '/* #endregion */';
|
||||
endProposal.documentation = l10n.t('Folding Region End');
|
||||
endProposal.sortText = 'zb';
|
||||
endProposal.filterText = match[2];
|
||||
return [beginProposal, endProposal];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
commands.registerCommand('_css.applyCodeAction', applyCodeAction);
|
||||
|
||||
function applyCodeAction(uri: string, documentVersion: number, edits: TextEdit[]) {
|
||||
const textEditor = window.activeTextEditor;
|
||||
if (textEditor && textEditor.document.uri.toString() === uri) {
|
||||
if (textEditor.document.version !== documentVersion) {
|
||||
window.showInformationMessage(l10n.t('CSS fix is outdated and can\'t be applied to the document.'));
|
||||
}
|
||||
textEditor.edit(mutator => {
|
||||
for (const edit of edits) {
|
||||
mutator.replace(client.protocol2CodeConverter.asRange(edit.range), edit.newText);
|
||||
}
|
||||
}).then(success => {
|
||||
if (!success) {
|
||||
window.showErrorMessage(l10n.t('Failed to apply CSS fix to the document. Please consider opening an issue with steps to reproduce.'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateFormatterRegistration(registration: FormatterRegistration) {
|
||||
const formatEnabled = workspace.getConfiguration().get(registration.settingId);
|
||||
if (!formatEnabled && registration.provider) {
|
||||
registration.provider.dispose();
|
||||
registration.provider = undefined;
|
||||
} else if (formatEnabled && !registration.provider) {
|
||||
registration.provider = languages.registerDocumentRangeFormattingEditProvider(registration.languageId, {
|
||||
provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {
|
||||
const filesConfig = workspace.getConfiguration('files', document);
|
||||
|
||||
const fileFormattingOptions = {
|
||||
trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),
|
||||
trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),
|
||||
insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),
|
||||
};
|
||||
const params: DocumentRangeFormattingParams = {
|
||||
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
|
||||
range: client.code2ProtocolConverter.asRange(range),
|
||||
options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions)
|
||||
};
|
||||
// add the css formatter options from the settings
|
||||
const formatterSettings = workspace.getConfiguration(registration.languageId, document).get<CSSFormatSettings>('format');
|
||||
if (formatterSettings) {
|
||||
for (const key of cssFormatSettingKeys) {
|
||||
const val = formatterSettings[key];
|
||||
if (val !== undefined && val !== null) {
|
||||
params.options[key] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(
|
||||
client.protocol2CodeConverter.asTextEdits,
|
||||
(error) => {
|
||||
client.handleFailedRequest(DocumentRangeFormattingRequest.type, undefined, error, []);
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { workspace, extensions, Uri, EventEmitter, Disposable } from 'vscode';
|
||||
import { Utils } from 'vscode-uri';
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function getCustomDataPathsInAllWorkspaces(): string[] {
|
||||
const workspaceFolders = workspace.workspaceFolders;
|
||||
|
||||
const dataPaths: string[] = [];
|
||||
|
||||
if (!workspaceFolders) {
|
||||
return dataPaths;
|
||||
}
|
||||
|
||||
const collect = (paths: string[] | undefined, rootFolder: Uri) => {
|
||||
if (Array.isArray(paths)) {
|
||||
for (const path of paths) {
|
||||
if (typeof path === 'string') {
|
||||
dataPaths.push(Utils.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;
|
||||
}
|
||||
|
||||
function getCustomDataPathsFromAllExtensions(): string[] {
|
||||
const dataPaths: string[] = [];
|
||||
for (const extension of extensions.all) {
|
||||
const customData = extension.packageJSON?.contributes?.css?.customData;
|
||||
if (Array.isArray(customData)) {
|
||||
for (const rp of customData) {
|
||||
dataPaths.push(Utils.joinPath(extension.extensionUri, rp).toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return dataPaths;
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { getDocumentDir, Mimes, Schemes } from './shared';
|
||||
import { UriList } from './uriList';
|
||||
|
||||
class DropOrPasteResourceProvider implements vscode.DocumentDropEditProvider, vscode.DocumentPasteEditProvider {
|
||||
|
||||
readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('css', 'link', 'url');
|
||||
|
||||
async provideDocumentDropEdits(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
dataTransfer: vscode.DataTransfer,
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<vscode.DocumentDropEdit | undefined> {
|
||||
const uriList = await this.getUriList(dataTransfer);
|
||||
if (!uriList.entries.length || token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snippet = await this.createUriListSnippet(document.uri, uriList);
|
||||
if (!snippet || token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: this.kind,
|
||||
title: snippet.label,
|
||||
insertText: snippet.snippet.value,
|
||||
yieldTo: this.pasteAsCssUrlByDefault(document, position) ? [] : [vscode.DocumentDropOrPasteEditKind.Empty.append('uri')]
|
||||
};
|
||||
}
|
||||
|
||||
async provideDocumentPasteEdits(
|
||||
document: vscode.TextDocument,
|
||||
ranges: readonly vscode.Range[],
|
||||
dataTransfer: vscode.DataTransfer,
|
||||
_context: vscode.DocumentPasteEditContext,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<vscode.DocumentPasteEdit[] | undefined> {
|
||||
const uriList = await this.getUriList(dataTransfer);
|
||||
if (!uriList.entries.length || token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snippet = await this.createUriListSnippet(document.uri, uriList);
|
||||
if (!snippet || token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [{
|
||||
kind: this.kind,
|
||||
title: snippet.label,
|
||||
insertText: snippet.snippet.value,
|
||||
yieldTo: this.pasteAsCssUrlByDefault(document, ranges[0].start) ? [] : [vscode.DocumentDropOrPasteEditKind.Empty.append('uri')]
|
||||
}];
|
||||
}
|
||||
|
||||
private async getUriList(dataTransfer: vscode.DataTransfer): Promise<UriList> {
|
||||
const urlList = await dataTransfer.get(Mimes.uriList)?.asString();
|
||||
if (urlList) {
|
||||
return UriList.from(urlList);
|
||||
}
|
||||
|
||||
// Find file entries
|
||||
const uris: vscode.Uri[] = [];
|
||||
for (const [_, entry] of dataTransfer) {
|
||||
const file = entry.asFile();
|
||||
if (file?.uri) {
|
||||
uris.push(file.uri);
|
||||
}
|
||||
}
|
||||
|
||||
return new UriList(uris.map(uri => ({ uri, str: uri.toString(true) })));
|
||||
}
|
||||
|
||||
private async createUriListSnippet(docUri: vscode.Uri, uriList: UriList): Promise<{ readonly snippet: vscode.SnippetString; readonly label: string } | undefined> {
|
||||
if (!uriList.entries.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snippet = new vscode.SnippetString();
|
||||
for (let i = 0; i < uriList.entries.length; i++) {
|
||||
const uri = uriList.entries[i];
|
||||
const relativePath = getRelativePath(getDocumentDir(docUri), uri.uri);
|
||||
const urlText = relativePath ?? uri.str;
|
||||
|
||||
snippet.appendText(`url(${urlText})`);
|
||||
if (i !== uriList.entries.length - 1) {
|
||||
snippet.appendText(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
snippet,
|
||||
label: uriList.entries.length > 1
|
||||
? vscode.l10n.t('Insert url() Functions')
|
||||
: vscode.l10n.t('Insert url() Function')
|
||||
};
|
||||
}
|
||||
|
||||
private pasteAsCssUrlByDefault(document: vscode.TextDocument, position: vscode.Position): boolean {
|
||||
const regex = /url\(.+?\)/gi;
|
||||
for (const match of Array.from(document.lineAt(position.line).text.matchAll(regex))) {
|
||||
if (position.character > match.index && position.character < match.index + match[0].length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function getRelativePath(fromFile: vscode.Uri | undefined, toFile: vscode.Uri): string | undefined {
|
||||
if (fromFile && fromFile.scheme === toFile.scheme && fromFile.authority === toFile.authority) {
|
||||
if (toFile.scheme === Schemes.file) {
|
||||
// On windows, we must use the native `path.relative` to generate the relative path
|
||||
// so that drive-letters are resolved cast insensitively. However we then want to
|
||||
// convert back to a posix path to insert in to the document
|
||||
const relativePath = path.relative(fromFile.fsPath, toFile.fsPath);
|
||||
return path.posix.normalize(relativePath.split(path.sep).join(path.posix.sep));
|
||||
}
|
||||
|
||||
return path.posix.relative(fromFile.path, toFile.path);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function registerDropOrPasteResourceSupport(selector: vscode.DocumentSelector): vscode.Disposable {
|
||||
const provider = new DropOrPasteResourceProvider();
|
||||
|
||||
return vscode.Disposable.from(
|
||||
vscode.languages.registerDocumentDropEditProvider(selector, provider, {
|
||||
providedDropEditKinds: [provider.kind],
|
||||
dropMimeTypes: [
|
||||
Mimes.uriList,
|
||||
'files'
|
||||
]
|
||||
}),
|
||||
vscode.languages.registerDocumentPasteEditProvider(selector, provider, {
|
||||
providedPasteEditKinds: [provider.kind],
|
||||
pasteMimeTypes: [
|
||||
Mimes.uriList,
|
||||
'files'
|
||||
]
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Utils } from 'vscode-uri';
|
||||
|
||||
export const Schemes = Object.freeze({
|
||||
file: 'file',
|
||||
notebookCell: 'vscode-notebook-cell',
|
||||
untitled: 'untitled',
|
||||
});
|
||||
|
||||
export const Mimes = Object.freeze({
|
||||
plain: 'text/plain',
|
||||
uriList: 'text/uri-list',
|
||||
});
|
||||
|
||||
|
||||
export function getDocumentDir(uri: vscode.Uri): vscode.Uri | undefined {
|
||||
const docUri = getParentDocumentUri(uri);
|
||||
if (docUri.scheme === Schemes.untitled) {
|
||||
return vscode.workspace.workspaceFolders?.[0]?.uri;
|
||||
}
|
||||
return Utils.dirname(docUri);
|
||||
}
|
||||
|
||||
function getParentDocumentUri(uri: vscode.Uri): vscode.Uri {
|
||||
if (uri.scheme === Schemes.notebookCell) {
|
||||
// is notebook documents necessary?
|
||||
for (const notebook of vscode.workspace.notebookDocuments) {
|
||||
for (const cell of notebook.getCells()) {
|
||||
if (cell.document.uri.toString() === uri.toString()) {
|
||||
return notebook.uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
function splitUriList(str: string): string[] {
|
||||
return str.split('\r\n');
|
||||
}
|
||||
|
||||
function parseUriList(str: string): string[] {
|
||||
return splitUriList(str)
|
||||
.filter(value => !value.startsWith('#')) // Remove comments
|
||||
.map(value => value.trim());
|
||||
}
|
||||
|
||||
export class UriList {
|
||||
|
||||
static from(str: string): UriList {
|
||||
return new UriList(coalesce(parseUriList(str).map(line => {
|
||||
try {
|
||||
return { uri: vscode.Uri.parse(line), str: line };
|
||||
} catch {
|
||||
// Uri parse failure
|
||||
return undefined;
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly entries: ReadonlyArray<{ readonly uri: vscode.Uri; readonly str: string }>
|
||||
) { }
|
||||
}
|
||||
|
||||
function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
|
||||
return <T[]>array.filter(e => !!e);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TextDecoder } from 'util';
|
||||
import { ExtensionContext, extensions, l10n } from 'vscode';
|
||||
import { BaseLanguageClient, LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node';
|
||||
import { LanguageClientConstructor, startClient } from '../cssClient';
|
||||
import { getNodeFSRequestService } from './nodeFs';
|
||||
import { registerDropOrPasteResourceSupport } from '../dropOrPaste/dropOrPasteResource';
|
||||
|
||||
let client: BaseLanguageClient | undefined;
|
||||
|
||||
// this method is called when vs code is activated
|
||||
export async function activate(context: ExtensionContext) {
|
||||
const clientMain = extensions.getExtension('vscode.css-language-features')?.packageJSON?.main || '';
|
||||
|
||||
const serverMain = `./server/${clientMain.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/cssServerMain`;
|
||||
const serverModule = context.asAbsolutePath(serverMain);
|
||||
|
||||
// The debug options for the server
|
||||
const debugOptions = { execArgv: ['--nolazy', '--inspect=' + (7000 + Math.round(Math.random() * 999))] };
|
||||
|
||||
// If the extension is launch in debug mode the debug server options are use
|
||||
// Otherwise the run options are used
|
||||
const serverOptions: ServerOptions = {
|
||||
run: { module: serverModule, transport: TransportKind.ipc },
|
||||
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
|
||||
};
|
||||
|
||||
const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => {
|
||||
return new LanguageClient(id, name, serverOptions, clientOptions);
|
||||
};
|
||||
|
||||
// pass the location of the localization bundle to the server
|
||||
process.env['VSCODE_L10N_BUNDLE_LOCATION'] = l10n.uri?.toString() ?? '';
|
||||
|
||||
client = await startClient(context, newLanguageClient, { fs: getNodeFSRequestService(), TextDecoder });
|
||||
|
||||
context.subscriptions.push(registerDropOrPasteResourceSupport({ language: 'css', scheme: '*' }));
|
||||
}
|
||||
|
||||
export async function deactivate(): Promise<void> {
|
||||
if (client) {
|
||||
await client.stop();
|
||||
client = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 { RequestService, FileType } from '../requests';
|
||||
|
||||
export function getNodeFSRequestService(): RequestService {
|
||||
function ensureFileUri(location: string) {
|
||||
if (!location.startsWith('file://')) {
|
||||
throw new Error('fileRequestService can only handle file URLs');
|
||||
}
|
||||
}
|
||||
return {
|
||||
getContent(location: string, encoding?: BufferEncoding) {
|
||||
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];
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Uri, workspace } from 'vscode';
|
||||
import { RequestType, BaseLanguageClient } from 'vscode-languageclient';
|
||||
import { Runtime } from './cssClient';
|
||||
|
||||
export namespace FsContentRequest {
|
||||
export const type: RequestType<{ uri: string; encoding?: string }, string, any> = new RequestType('fs/content');
|
||||
}
|
||||
export namespace FsStatRequest {
|
||||
export const type: RequestType<string, FileStat, any> = new RequestType('fs/stat');
|
||||
}
|
||||
|
||||
export namespace FsReadDirRequest {
|
||||
export const type: RequestType<string, [string, FileType][], any> = new RequestType('fs/readDir');
|
||||
}
|
||||
|
||||
export function serveFileSystemRequests(client: BaseLanguageClient, runtime: Runtime) {
|
||||
client.onRequest(FsContentRequest.type, (param: { uri: string; encoding?: string }) => {
|
||||
const uri = Uri.parse(param.uri);
|
||||
if (uri.scheme === 'file' && runtime.fs) {
|
||||
return runtime.fs.getContent(param.uri);
|
||||
}
|
||||
return workspace.fs.readFile(uri).then(buffer => {
|
||||
return new runtime.TextDecoder(param.encoding).decode(buffer);
|
||||
});
|
||||
});
|
||||
client.onRequest(FsReadDirRequest.type, (uriString: string) => {
|
||||
const uri = Uri.parse(uriString);
|
||||
if (uri.scheme === 'file' && runtime.fs) {
|
||||
return runtime.fs.readDirectory(uriString);
|
||||
}
|
||||
return workspace.fs.readDirectory(uri);
|
||||
});
|
||||
client.onRequest(FsStatRequest.type, (uriString: string) => {
|
||||
const uri = Uri.parse(uriString);
|
||||
if (uri.scheme === 'file' && runtime.fs) {
|
||||
return runtime.fs.stat(uriString);
|
||||
}
|
||||
return workspace.fs.stat(uri);
|
||||
});
|
||||
}
|
||||
|
||||
export enum FileType {
|
||||
/**
|
||||
* The file type is unknown.
|
||||
*/
|
||||
Unknown = 0,
|
||||
/**
|
||||
* A regular file.
|
||||
*/
|
||||
File = 1,
|
||||
/**
|
||||
* A directory.
|
||||
*/
|
||||
Directory = 2,
|
||||
/**
|
||||
* A symbolic link to a file.
|
||||
*/
|
||||
SymbolicLink = 64
|
||||
}
|
||||
export interface FileStat {
|
||||
/**
|
||||
* The type of the file, e.g. is a regular file, a directory, or symbolic link
|
||||
* to a file.
|
||||
*/
|
||||
type: FileType;
|
||||
/**
|
||||
* The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
|
||||
*/
|
||||
ctime: number;
|
||||
/**
|
||||
* The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
|
||||
*/
|
||||
mtime: number;
|
||||
/**
|
||||
* The size in bytes.
|
||||
*/
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface RequestService {
|
||||
getContent(uri: string, encoding?: string): Promise<string>;
|
||||
|
||||
stat(uri: string): Promise<FileStat>;
|
||||
readDirectory(uri: string): Promise<[string, FileType][]>;
|
||||
}
|
||||
Reference in New Issue
Block a user