Skip to content

Commit

Permalink
feat: Support Format Document action (#200)
Browse files Browse the repository at this point in the history
fixes #25
  • Loading branch information
adalinesimonian committed Sep 13, 2021
1 parent 331646b commit 39d8569
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 4 deletions.
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) => {
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;
}

0 comments on commit 39d8569

Please sign in to comment.