Skip to content

Commit

Permalink
Merge pull request #115 from stylelint/fix-snippets
Browse files Browse the repository at this point in the history
  • Loading branch information
ntwb authored Jun 8, 2020
2 parents f204d8d + e6a3c09 commit 115118b
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 84 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ Default: `"npm"`

Controls the package manager to be used to resolve the stylelint library. This has only an influence if the stylelint library is resolved globally. Valid values are `"npm"` or `"yarn"` or `"pnpm"`.

#### stylelint.snippet

Type: `string[]`
Default: `["css","less","postcss","scss"]`

An array of language identifiers specifying the files to enable snippets.

#### editor.codeActionsOnSave

This setting supports the entry `source.fixAll.stylelint`. If set to `true` all auto-fixable stylelint errors will be fixed on save.
Expand Down
32 changes: 14 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,20 @@
"xsl"
],
"description": "An array of language ids which should be validated by stylelint."
},
"stylelint.snippet": {
"scope": "resource",
"type": "array",
"items": {
"type": "string"
},
"default": [
"css",
"less",
"postcss",
"scss"
],
"description": "An array of language ids which snippets are provided by stylelint."
}
}
},
Expand All @@ -149,24 +163,6 @@
".stylelintignore"
]
}
],
"snippets": [
{
"language": "css",
"path": "./snippets/stylelint-disable.json"
},
{
"language": "less",
"path": "./snippets/stylelint-disable.json"
},
{
"language": "postcss",
"path": "./snippets/stylelint-disable.json"
},
{
"language": "scss",
"path": "./snippets/stylelint-disable.json"
}
]
},
"dependencies": {
Expand Down
194 changes: 194 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ const {
CodeActionKind,
TextDocumentEdit,
CodeAction,
CompletionItemKind,
DiagnosticCode,
MarkupKind,
InsertTextFormat,
} = require('vscode-languageserver');
const { TextDocument } = require('vscode-languageserver-textdocument');

/**
* @typedef { import('vscode-languageserver').DocumentUri } DocumentUri
* @typedef { import('vscode-languageserver').Diagnostic } Diagnostic
* @typedef { import('vscode-languageserver').CompletionItem } CompletionItem
* @typedef { import('vscode-languageserver').CompletionParams } CompletionParams
* @typedef { import('vscode-languageserver-textdocument').TextDocument } TextDocument
* @typedef { import('./lib/stylelint-vscode').DisableReportRange } DisableReportRange
* @typedef { import('stylelint').Configuration } StylelintConfiguration
Expand Down Expand Up @@ -57,10 +63,16 @@ let reportInvalidScopeDisables;
let stylelintPath;
/** @type {string[]} */
let validateLanguages;
/** @type {string[]} */
let snippetLanguages;

const connection = createConnection(ProposedFeatures.all);
const documents = new TextDocuments(TextDocument);

/**
* @type {Map<DocumentUri, Diagnostic[]>}
*/
const documentDiagnostics = new Map();
/**
* @type {Map<DocumentUri, ({ diagnostic: Diagnostic, range: DisableReportRange })[]>}
*/
Expand Down Expand Up @@ -187,6 +199,7 @@ async function validate(document) {
uri: document.uri,
diagnostics: result.diagnostics,
});
documentDiagnostics.set(document.uri, result.diagnostics);

if (result.needlessDisables) {
needlessDisableReports.set(document.uri, result.needlessDisables);
Expand Down Expand Up @@ -251,6 +264,7 @@ function clearDiagnostics(document) {
uri: document.uri,
diagnostics: [],
});
documentDiagnostics.delete(document.uri);
needlessDisableReports.delete(document.uri);
invalidScopeDisableReports.delete(document.uri);
}
Expand All @@ -276,6 +290,7 @@ connection.onInitialize(() => {
commands: [CommandIds.applyAutoFix],
},
codeActionProvider: { codeActionKinds: [CodeActionKind.QuickFix, StylelintSourceFixAll] },
completionProvider: {},
},
};
});
Expand All @@ -291,6 +306,7 @@ connection.onDidChangeConfiguration(({ settings }) => {
stylelintPath = settings.stylelint.stylelintPath;
packageManager = settings.stylelint.packageManager || 'npm';
validateLanguages = settings.stylelint.validate || [];
snippetLanguages = settings.stylelint.snippet || ['css', 'less', 'postcss', 'scss'];

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

Expand Down Expand Up @@ -429,6 +445,8 @@ connection.onCodeAction(async (params) => {
}
});

connection.onCompletion(onCompletion);

documents.listen(connection);

connection.listen();
Expand Down Expand Up @@ -499,6 +517,182 @@ function replaceEdits(document, newText) {
return edits;
}

/**
* @param {CompletionParams} params
* @returns {CompletionItem[]}
*/
function onCompletion(params) {
const uri = params.textDocument.uri;
const document = documents.get(uri);

if (!document || !isValidateOn(document) || !snippetLanguages.includes(document.languageId)) {
return [];
}

const diagnostics = documentDiagnostics.get(uri);

if (!diagnostics) {
return [
createDisableLineCompletionItem('stylelint-disable-line'),
createDisableLineCompletionItem('stylelint-disable-next-line'),
createDisableEnableCompletionItem(),
];
}

/** @type {Set<string>} */
const needlessDisablesKeys = new Set();
const needlessDisables = needlessDisableReports.get(uri);

if (needlessDisables) {
for (const needlessDisable of needlessDisables) {
needlessDisablesKeys.add(computeKey(needlessDisable.diagnostic));
}
}

const thisLineRules = new Set();
const nextLineRules = new Set();

for (const diagnostic of diagnostics) {
if (needlessDisablesKeys.has(computeKey(diagnostic))) {
continue;
}

const start = diagnostic.range.start;

const rule = String(
(DiagnosticCode.is(diagnostic.code) ? diagnostic.code.value : diagnostic.code) || '',
);

if (start.line === params.position.line) {
thisLineRules.add(rule);
} else if (start.line === params.position.line + 1) {
nextLineRules.add(rule);
}
}

thisLineRules.delete('');
thisLineRules.delete('CssSyntaxError');
nextLineRules.delete('');
nextLineRules.delete('CssSyntaxError');

/** @type {CompletionItem[]} */
const results = [];

const disableKind = getStyleLintDisableKind(document, params.position);

if (disableKind) {
if (disableKind === 'stylelint-disable-line') {
for (const rule of thisLineRules) {
results.push({
label: rule,
kind: CompletionItemKind.Snippet,
detail: `disable ${rule} rule. (stylelint)`,
});
}
} else if (
disableKind === 'stylelint-disable' ||
disableKind === 'stylelint-disable-next-line'
) {
for (const rule of nextLineRules) {
results.push({
label: rule,
kind: CompletionItemKind.Snippet,
detail: `disable ${rule} rule. (stylelint)`,
});
}
}
} else {
if (thisLineRules.size === 1) {
results.push(
createDisableLineCompletionItem('stylelint-disable-line', [...thisLineRules][0]),
);
} else {
results.push(createDisableLineCompletionItem('stylelint-disable-line'));
}

if (nextLineRules.size === 1) {
results.push(
createDisableLineCompletionItem('stylelint-disable-next-line', [...nextLineRules][0]),
);
} else {
results.push(createDisableLineCompletionItem('stylelint-disable-next-line'));
}

results.push(createDisableEnableCompletionItem());
}

return results;
}

/**
* @param { 'stylelint-disable-line' | 'stylelint-disable-next-line' } kind
* @param {string} rule
* @returns {CompletionItem}
*/
function createDisableLineCompletionItem(kind, rule = '') {
return {
label: kind,
kind: CompletionItemKind.Snippet,
insertText: `/* ${kind} \${0:${rule || 'rule'}} */`,
insertTextFormat: InsertTextFormat.Snippet,
detail:
kind === 'stylelint-disable-line'
? 'Turn off stylelint rules for individual lines only, after which you do not need to explicitly re-enable them. (stylelint)'
: 'Turn off stylelint rules for the next line only, after which you do not need to explicitly re-enable them. (stylelint)',
documentation: {
kind: MarkupKind.Markdown,
value: `\`\`\`css\n/* ${kind} ${rule || 'rule'} */\n\`\`\``,
},
};
}

/**
* @returns {CompletionItem}
*/
function createDisableEnableCompletionItem() {
return {
label: 'stylelint-disable',
kind: CompletionItemKind.Snippet,
insertText: `/* stylelint-disable \${0:rule} */\n/* stylelint-enable \${0:rule} */`,
insertTextFormat: InsertTextFormat.Snippet,
detail:
'Turn off all stylelint or individual rules, after which you do not need to re-enable stylelint. (stylelint)',
documentation: {
kind: MarkupKind.Markdown,
value: `\`\`\`css\n/* stylelint-disable rule */\n/* stylelint-enable rule */\n\`\`\``,
},
};
}

/**
* Check if the given position is in the stylelint-disable comment.
* If inside a comment, return the kind of disable.
* @param {TextDocument} document
* @param {Position} position
*/
function getStyleLintDisableKind(document, position) {
const lineStartOffset = document.offsetAt(Position.create(position.line, 0));
const lineEndOffset = document.offsetAt(Position.create(position.line + 1, 0)) - 1;
const line = document.getText().slice(lineStartOffset, lineEndOffset);

const before = line.slice(0, position.character);
const after = line.slice(position.character);

const disableKindResult = /\/\*\s*(stylelint-disable(?:(?:-next)?-line)?)\s+[a-z\-/\s,]*$/i.exec(
before,
);

if (!disableKindResult) {
return null;
}

if (/^[a-z\-/\s,]*\*\//i.test(after)) {
return disableKindResult[1];
}

return null;
}

/**
* @param {TextDocument} document
* @param {DisableReportRange} range
Expand Down
17 changes: 0 additions & 17 deletions snippets/stylelint-disable.json

This file was deleted.

49 changes: 0 additions & 49 deletions snippets/stylelint-disable.test.js

This file was deleted.

0 comments on commit 115118b

Please sign in to comment.