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

Add support for Format Document action #200

Merged
merged 1 commit into from
Sep 13, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
73 changes: 71 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
'use strict';

const { LanguageClient, SettingMonitor, ExecuteCommandRequest } = require('vscode-languageclient');
const { workspace, commands: Commands, window: Window } = require('vscode');
const {
LanguageClient,
SettingMonitor,
ExecuteCommandRequest,
DocumentFormattingRequest,
TextDocumentIdentifier,
} = require('vscode-languageclient');
const { workspace, commands: Commands, window: Window, languages: Languages } = require('vscode');

/**
* @typedef {import('vscode').ExtensionContext} ExtensionContext
Expand Down Expand Up @@ -37,6 +43,69 @@ exports.activate = ({ subscriptions }) => {
},
);

client.onReady().then(() => {
/**
* Map of registered formatters by language ID.
* @type {Map<string, { dispose(): any }>}
*/
const registeredFormatters = new Map();

client.onNotification('stylelint/languageIdsAdded', (/** @type {string[]} */ langIds) => {
adalinesimonian marked this conversation as resolved.
Show resolved Hide resolved
for (const langId of langIds) {
// Avoid registering another formatter if we already registered one for the same language ID.
if (registeredFormatters.has(langId)) {
return;
}

const formatter = Languages.registerDocumentFormattingEditProvider(langId, {
provideDocumentFormattingEdits(textDocument, options) {
const params = {
textDocument: TextDocumentIdentifier.create(textDocument.uri.toString()),
options, // Editor formatting options, overriden by stylelint config.
};

// Request that the language server formats the document.
return client
.sendRequest(DocumentFormattingRequest.type, params)
.then(undefined, () => {
Window.showErrorMessage(
'Failed to format the document using stylelint. Please consider opening an issue with steps to reproduce.',
);

return null;
});
},
});

// Keep track of the new formatter.
registeredFormatters.set(langId, formatter);
}
});

client.onNotification('stylelint/languageIdsRemoved', (/** @type {string[]} */ langIds) => {
for (const langId of langIds) {
const formatter = registeredFormatters.get(langId);

if (!formatter) {
return;
}

// Unregisters formatter.
formatter.dispose();
registeredFormatters.delete(langId);
}
});

// Make sure that formatters are disposed when extension is unloaded.
subscriptions.push({
dispose() {
for (const formatter of registeredFormatters.values()) {
formatter.dispose();
}
},
});
});

subscriptions.push(
Commands.registerCommand('stylelint.executeAutofix', async () => {
const textEditor = Window.activeTextEditor;
Expand Down
57 changes: 55 additions & 2 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,36 @@ async function validate(document) {

/**
* @param {TextDocument} document
* @param {import('vscode-languageserver').FormattingOptions?} formattingOptions Formatting options to use.
* Overriden by stylelint configuration.
* @returns {Promise<TextEdit[]>}
*/
async function getFixes(document) {
const options = await buildStylelintOptions(document, { fix: true });
async function getFixes(document, formattingOptions = null) {
/** @type {Partial<import('stylelint').LinterOptions>} */
const baseOptions = { fix: true };

// If formatting options were provided, translate them to their corresponding rules.
// NOTE: There is no equivalent rule for trimFinalNewlines, so it is not respected.
if (formattingOptions) {
const { insertSpaces, tabSize, insertFinalNewline, trimTrailingWhitespace } = formattingOptions;

/** @type {Record<string, any>} */
const rules = {
indentation: [insertSpaces ? tabSize : 'tab'],
};

if (insertFinalNewline !== undefined) {
rules['no-missing-end-of-source-newline'] = insertFinalNewline;
}

if (trimTrailingWhitespace !== undefined) {
rules['no-eol-whitespace'] = trimTrailingWhitespace;
}

baseOptions.config = { rules };
}

const options = await buildStylelintOptions(document, baseOptions);

try {
const result = await stylelintVSCode(
Expand Down Expand Up @@ -319,6 +345,7 @@ connection.onInitialize(() => {
executeCommandProvider: {
commands: [CommandIds.applyAutoFix],
},
documentFormattingProvider: true,
codeActionProvider: { codeActionKinds: [CodeActionKind.QuickFix, StylelintSourceFixAll] },
completionProvider: {},
},
Expand Down Expand Up @@ -350,6 +377,16 @@ connection.onDidChangeConfiguration(({ settings }) => {
clearDiagnostics(document);
}

if (removeLanguages.length > 0) {
connection.sendNotification('stylelint/languageIdsRemoved', removeLanguages);
}

const addLanguages = validateLanguages.filter((lang) => !oldValidateLanguages.includes(lang));

if (addLanguages.length > 0) {
connection.sendNotification('stylelint/languageIdsAdded', addLanguages);
}

validateAll();
});
connection.onDidChangeWatchedFiles(validateAll);
Expand Down Expand Up @@ -400,6 +437,22 @@ connection.onExecuteCommand(async (params) => {

return {};
});
connection.onDocumentFormatting((params) => {
if (!params.textDocument) {
return null;
}

/** @type { { uri: string } } */
const identifier = params.textDocument;
const uri = identifier.uri;
const document = documents.get(uri);

if (!document || !isValidateOn(document)) {
return null;
}

return getFixes(document, params.options);
});
connection.onCodeAction(async (params) => {
const only = params.context.only !== undefined ? params.context.only[0] : undefined;
const isSource = only === CodeActionKind.Source;
Expand Down
5 changes: 5 additions & 0 deletions test/ws-formatter-test/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"[css]": {
"editor.defaultFormatter": "stylelint.vscode-stylelint"
}
}
41 changes: 41 additions & 0 deletions test/ws-formatter-test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';

const fs = require('fs/promises');
const path = require('path');

const pWaitFor = require('p-wait-for');
const test = require('tape');
const { extensions, workspace, Uri, commands, window } = require('vscode');

const run = () =>
test('vscode-stylelint', async (t) => {
const expectedCss = await fs.readFile(path.resolve(__dirname, 'test.expected.css'), 'utf8');

await commands.executeCommand('vscode.openFolder', Uri.file(__dirname));

const vscodeStylelint = extensions.getExtension('stylelint.vscode-stylelint');

// Open the './test.input.css' file.
const cssDocument = await workspace.openTextDocument(path.resolve(__dirname, 'test.input.css'));

await window.showTextDocument(cssDocument);

await commands.executeCommand('editor.action.indentUsingTabs', {
tabSize: 4,
indentSize: 4,
insertSpaces: false,
});

await pWaitFor(() => vscodeStylelint.isActive, { timeout: 2000 });

await commands.executeCommand('editor.action.formatDocument');

t.equal(cssDocument.getText(), expectedCss, 'should format document using formatting options.');

t.end();
});

exports.run = (root, done) => {
test.onFinish(done);
run();
};
3 changes: 3 additions & 0 deletions test/ws-formatter-test/test.expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
a {
color: red;
}
3 changes: 3 additions & 0 deletions test/ws-formatter-test/test.input.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
a {
color: red;
}