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 CodeAction to convert Tab characters to spaces #416

Merged
merged 2 commits into from
Mar 1, 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
8 changes: 7 additions & 1 deletion src/languageservice/parser/jsonParser07.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,13 @@ export class JSONDocument {
textDocument.positionAt(p.location.offset),
textDocument.positionAt(p.location.offset + p.location.length)
);
const diagnostic: Diagnostic = Diagnostic.create(range, p.message, p.severity, p.code, p.source);
const diagnostic: Diagnostic = Diagnostic.create(
range,
p.message,
p.severity,
p.code ? p.code : ErrorCode.Undefined,
p.source
);
diagnostic.data = { schemaUri: p.schemaUri };
return diagnostic;
});
Expand Down
120 changes: 119 additions & 1 deletion src/languageservice/services/yamlCodeActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,31 @@
*--------------------------------------------------------------------------------------------*/

import { TextDocument } from 'vscode-languageserver-textdocument';
import { ClientCapabilities, CodeAction, CodeActionParams, Command, Connection, Diagnostic } from 'vscode-languageserver';
import {
ClientCapabilities,
CodeAction,
CodeActionKind,
CodeActionParams,
Command,
Connection,
Diagnostic,
Position,
Range,
TextEdit,
WorkspaceEdit,
} from 'vscode-languageserver';
import { YamlCommands } from '../../commands';
import * as path from 'path';
import { CommandExecutor } from '../../languageserver/commandExecutor';
import { TextBuffer } from '../utils/textBuffer';
import { LanguageSettings } from '../yamlLanguageService';

interface YamlDiagnosticData {
schemaUri: string[];
}
export class YamlCodeActions {
private indentation = ' ';

constructor(commandExecutor: CommandExecutor, connection: Connection, private readonly clientCapabilities: ClientCapabilities) {
commandExecutor.registerCommand(YamlCommands.JUMP_TO_SCHEMA, async (uri: string) => {
if (!uri) {
Expand All @@ -29,6 +45,10 @@ export class YamlCodeActions {
});
}

configure(settings: LanguageSettings): void {
this.indentation = settings.indentation;
}

getCodeAction(document: TextDocument, params: CodeActionParams): CodeAction[] | undefined {
if (!params.context.diagnostics) {
return;
Expand All @@ -37,6 +57,7 @@ export class YamlCodeActions {
const result = [];

result.push(...this.getJumpToSchemaActions(params.context.diagnostics));
result.push(...this.getTabToSpaceConverting(params.context.diagnostics, document));

return result;
}
Expand Down Expand Up @@ -70,4 +91,101 @@ export class YamlCodeActions {

return result;
}

private getTabToSpaceConverting(diagnostics: Diagnostic[], document: TextDocument): CodeAction[] {
const result: CodeAction[] = [];
const textBuff = new TextBuffer(document);
const processedLine: number[] = [];
for (const diag of diagnostics) {
if (diag.message === 'Using tabs can lead to unpredictable results') {
if (processedLine.includes(diag.range.start.line)) {
continue;
}
const lineContent = textBuff.getLineContent(diag.range.start.line);
let replacedTabs = 0;
let newText = '';
for (let i = diag.range.start.character; i <= diag.range.end.character; i++) {
const char = lineContent.charAt(i);
if (char !== '\t') {
break;
}
replacedTabs++;
newText += this.indentation;
}
processedLine.push(diag.range.start.line);

let resultRange = diag.range;
if (replacedTabs !== diag.range.end.character - diag.range.start.character) {
resultRange = Range.create(
diag.range.start,
Position.create(diag.range.end.line, diag.range.start.character + replacedTabs)
);
}
result.push(
CodeAction.create(
'Convert Tab to Spaces',
createWorkspaceEdit(document.uri, [TextEdit.replace(resultRange, newText)]),
CodeActionKind.QuickFix
)
);
}
}

if (result.length !== 0) {
const replaceEdits: TextEdit[] = [];
for (let i = 0; i <= textBuff.getLineCount(); i++) {
const lineContent = textBuff.getLineContent(i);
let replacedTabs = 0;
let newText = '';
for (let j = 0; j < lineContent.length; j++) {
const char = lineContent.charAt(j);

if (char !== ' ' && char !== '\t') {
if (replacedTabs !== 0) {
replaceEdits.push(TextEdit.replace(Range.create(i, j - replacedTabs, i, j), newText));
replacedTabs = 0;
newText = '';
}
break;
}

if (char === ' ' && replacedTabs !== 0) {
replaceEdits.push(TextEdit.replace(Range.create(i, j - replacedTabs, i, j), newText));
replacedTabs = 0;
newText = '';
continue;
}
if (char === '\t') {
newText += this.indentation;
replacedTabs++;
}
}
// line contains only tabs
if (replacedTabs !== 0) {
replaceEdits.push(TextEdit.replace(Range.create(i, 0, i, textBuff.getLineLength(i)), newText));
}
}
if (replaceEdits.length > 0) {
result.push(
CodeAction.create(
'Convert all Tabs to Spaces',
createWorkspaceEdit(document.uri, replaceEdits),
CodeActionKind.QuickFix
)
);
}
}

return result;
}
}

function createWorkspaceEdit(uri: string, edits: TextEdit[]): WorkspaceEdit {
const changes = {};
changes[uri] = edits;
const edit: WorkspaceEdit = {
changes,
};

return edit;
}
25 changes: 21 additions & 4 deletions src/languageservice/services/yamlValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*--------------------------------------------------------------------------------------------*/
'use strict';

import { Diagnostic } from 'vscode-languageserver';
import { Diagnostic, Position } from 'vscode-languageserver';
import { LanguageSettings } from '../yamlLanguageService';
import { parse as parseYAML, YAMLDocument } from '../parser/yamlParser07';
import { SingleYAMLDocument } from '../parser/yamlParser07';
Expand All @@ -14,19 +14,23 @@ import { YAMLDocDiagnostic } from '../utils/parseUtils';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { JSONValidation } from 'vscode-json-languageservice/lib/umd/services/jsonValidation';
import { YAML_SOURCE } from '../parser/jsonParser07';
import { TextBuffer } from '../utils/textBuffer';

/**
* Convert a YAMLDocDiagnostic to a language server Diagnostic
* @param yamlDiag A YAMLDocDiagnostic from the parser
* @param textDocument TextDocument from the language server client
*/
export const yamlDiagToLSDiag = (yamlDiag: YAMLDocDiagnostic, textDocument: TextDocument): Diagnostic => {
const start = textDocument.positionAt(yamlDiag.location.start);
const range = {
start: textDocument.positionAt(yamlDiag.location.start),
end: textDocument.positionAt(yamlDiag.location.end),
start,
end: yamlDiag.location.toLineEnd
? Position.create(start.line, new TextBuffer(textDocument).getLineLength(yamlDiag.location.start))
: textDocument.positionAt(yamlDiag.location.end),
};

return Diagnostic.create(range, yamlDiag.message, yamlDiag.severity, undefined, YAML_SOURCE);
return Diagnostic.create(range, yamlDiag.message, yamlDiag.severity, yamlDiag.code, YAML_SOURCE);
};

export class YAMLValidation {
Expand Down Expand Up @@ -76,6 +80,7 @@ export class YAMLValidation {
index++;
}

let previousErr: Diagnostic;
const foundSignatures = new Set();
const duplicateMessagesRemoved: Diagnostic[] = [];
for (let err of validationResult) {
Expand All @@ -96,6 +101,18 @@ export class YAMLValidation {
err.source = YAML_SOURCE;
}

if (
previousErr &&
previousErr.message === err.message &&
previousErr.range.end.line === err.range.start.line &&
Math.abs(previousErr.range.end.character - err.range.end.character) >= 1
) {
previousErr.range.end = err.range.end;
continue;
} else {
previousErr = err;
}

const errSig = err.range.start.line + ' ' + err.range.start.character + ' ' + err.message;
if (!foundSignatures.has(errSig)) {
duplicateMessagesRemoved.push(err);
Expand Down
7 changes: 6 additions & 1 deletion src/languageservice/utils/parseUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Yaml from 'yaml-language-server-parser';
import { Schema, Type } from 'js-yaml';

import { filterInvalidCustomTags } from './arrUtils';
import { ErrorCode } from 'vscode-json-languageservice/lib/umd/jsonLanguageTypes';

export const DUPLICATE_KEY_REASON = 'duplicate key';

Expand All @@ -14,9 +15,11 @@ export interface YAMLDocDiagnostic {
location: {
start: number;
end: number;
toLineEnd: boolean;
};
severity: 1 | 2;
source?: string;
code: ErrorCode;
}

/**
Expand All @@ -29,9 +32,11 @@ function exceptionToDiagnostic(e: Yaml.YAMLException): YAMLDocDiagnostic {
message: `${e.reason}`,
location: {
start: e.mark.position,
end: e.mark.position + e.mark.column,
end: e.mark.position + 1, // we do not know actual end of error, so assuming that it 1 character
toLineEnd: e.mark.toLineEnd,
},
severity: 2,
code: ErrorCode.Undefined,
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/languageservice/utils/textBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { TextDocument } from 'vscode-languageserver';
import { TextDocument } from 'vscode-json-languageservice';
import { Range } from 'vscode-languageserver-types';

interface FullTextDocument {
Expand Down
1 change: 1 addition & 0 deletions src/languageservice/yamlLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export function getLanguageService(
const customTagsSetting = settings && settings['customTags'] ? settings['customTags'] : [];
completer.configure(settings, customTagsSetting);
formatter.configure(settings);
yamlCodeActions.configure(settings);
},
registerCustomSchemaProvider: (schemaProvider: CustomSchemaProvider) => {
schemaService.registerCustomSchemaProvider(schemaProvider);
Expand Down
2 changes: 1 addition & 1 deletion test/customTags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe('Custom Tag tests Tests', () => {
validator
.then(function (result) {
assert.equal(result.length, 1);
assert.deepEqual(result[0], createExpectedError('unknown tag <!test>', 0, 0, 0, 0));
assert.deepEqual(result[0], createExpectedError('unknown tag <!test>', 0, 0, 0, 5));
})
.then(done, done);
});
Expand Down
4 changes: 2 additions & 2 deletions test/schemaValidation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -773,8 +773,8 @@ describe('Validation Tests', () => {
validator
.then(function (result) {
assert.equal(result.length, 2);
assert.deepEqual(result[0], createExpectedError(DuplicateKeyError, 2, 0, 2, 0));
assert.deepEqual(result[1], createExpectedError(DuplicateKeyError, 0, 0, 0, 0));
assert.deepEqual(result[0], createExpectedError(DuplicateKeyError, 2, 0, 2, 1));
assert.deepEqual(result[1], createExpectedError(DuplicateKeyError, 0, 0, 0, 1));
})
.then(done, done);
});
Expand Down
6 changes: 4 additions & 2 deletions test/utils/verifyError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { DocumentSymbol, SymbolKind, InsertTextFormat, Range } from 'vscode-languageserver-types';
import { CompletionItem, CompletionItemKind, SymbolInformation, Diagnostic, DiagnosticSeverity } from 'vscode-languageserver';
import { ErrorCode } from 'vscode-json-languageservice';

export function createExpectedError(
message: string,
Expand All @@ -13,9 +14,10 @@ export function createExpectedError(
endLine: number,
endCharacter: number,
severity: DiagnosticSeverity = 1,
source = 'YAML'
source = 'YAML',
code = ErrorCode.Undefined
): Diagnostic {
return Diagnostic.create(Range.create(startLine, startCharacter, endLine, endCharacter), message, severity, undefined, source);
return Diagnostic.create(Range.create(startLine, startCharacter, endLine, endCharacter), message, severity, code, source);
}

export function createDiagnosticWithData(
Expand Down
58 changes: 57 additions & 1 deletion test/yamlCodeActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ import {
CodeActionParams,
Command,
Connection,
Range,
TextDocumentIdentifier,
TextEdit,
WorkspaceEdit,
} from 'vscode-languageserver';
import { setupTextDocument, TEST_URI } from './utils/testHelper';
import { createDiagnosticWithData } from './utils/verifyError';
import { createDiagnosticWithData, createExpectedError } from './utils/verifyError';
import { YamlCommands } from '../src/commands';
import { LanguageSettings } from '../src';

const expect = chai.expect;
chai.use(sinonChai);
Expand Down Expand Up @@ -121,4 +125,56 @@ describe('CodeActions Tests', () => {
expect(result[1]).to.deep.equal(codeAction2);
});
});

describe('Convert TAB to Spaces', () => {
it('should add "Convert TAB to Spaces" CodeAction', () => {
const doc = setupTextDocument('foo:\n\t- bar');
const diagnostics = [createExpectedError('Using tabs can lead to unpredictable results', 1, 0, 1, 1, 1, JSON_SCHEMA_LOCAL)];
const params: CodeActionParams = {
context: CodeActionContext.create(diagnostics),
range: undefined,
textDocument: TextDocumentIdentifier.create(TEST_URI),
};
const actions = new YamlCodeActions(commandExecutor, ({} as unknown) as Connection, clientCapabilities);
const result = actions.getCodeAction(doc, params);
expect(result).to.has.length(2);
expect(result[0].title).to.be.equal('Convert Tab to Spaces');
expect(WorkspaceEdit.is(result[0].edit)).to.be.true;
expect(result[0].edit.changes[TEST_URI]).deep.equal([TextEdit.replace(Range.create(1, 0, 1, 1), ' ')]);
});

it('should support current indentation chars settings', () => {
const doc = setupTextDocument('foo:\n\t- bar');
const diagnostics = [createExpectedError('Using tabs can lead to unpredictable results', 1, 0, 1, 1, 1, JSON_SCHEMA_LOCAL)];
const params: CodeActionParams = {
context: CodeActionContext.create(diagnostics),
range: undefined,
textDocument: TextDocumentIdentifier.create(TEST_URI),
};
const actions = new YamlCodeActions(commandExecutor, ({} as unknown) as Connection, clientCapabilities);
actions.configure({ indentation: ' ' } as LanguageSettings);
const result = actions.getCodeAction(doc, params);

expect(result[0].title).to.be.equal('Convert Tab to Spaces');
expect(result[0].edit.changes[TEST_URI]).deep.equal([TextEdit.replace(Range.create(1, 0, 1, 1), ' ')]);
});

it('should provide "Convert all Tabs to Spaces"', () => {
const doc = setupTextDocument('foo:\n\t\t\t- bar\n\t\t');
const diagnostics = [createExpectedError('Using tabs can lead to unpredictable results', 1, 0, 1, 3, 1, JSON_SCHEMA_LOCAL)];
const params: CodeActionParams = {
context: CodeActionContext.create(diagnostics),
range: undefined,
textDocument: TextDocumentIdentifier.create(TEST_URI),
};
const actions = new YamlCodeActions(commandExecutor, ({} as unknown) as Connection, clientCapabilities);
const result = actions.getCodeAction(doc, params);

expect(result[1].title).to.be.equal('Convert all Tabs to Spaces');
expect(result[1].edit.changes[TEST_URI]).deep.equal([
TextEdit.replace(Range.create(1, 0, 1, 3), ' '),
TextEdit.replace(Range.create(2, 0, 2, 2), ' '),
]);
});
});
});
Loading