Skip to content
This repository has been archived by the owner on Jun 15, 2021. It is now read-only.

Commit

Permalink
feat: added formatting service (#73)
Browse files Browse the repository at this point in the history
Closes #20
  • Loading branch information
OmarTawfik authored Mar 14, 2019
1 parent eb633c8 commit b1092dc
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { FoldingService } from "./services/folding";
import { RenamingService } from "./services/renaming";
import { FindReferencesService } from "./services/find-references";
import { GoToDefinitionService } from "./services/go-to-definition";
import { FormattingService } from "./services/formatting";

export interface LanguageService {
activate(connection: IConnection, documents: TextDocuments): void;
Expand All @@ -29,6 +30,7 @@ const services: ReadonlyArray<LanguageService> = [
new DiagnosticsService(),
new FindReferencesService(),
new FoldingService(),
new FormattingService(),
new GoToDefinitionService(),
new RenamingService(),
];
Expand Down
125 changes: 125 additions & 0 deletions src/services/formatting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*!
* Copyright 2019 Omar Tawfik. Please see LICENSE file at the root of this repository.
*/

import { IConnection, TextDocuments, ServerCapabilities, TextEdit, Range } from "vscode-languageserver";
import { LanguageService } from "../server";
import { accessCache } from "../util/cache";
import { Compilation } from "../util/compilation";
import { Token, TokenKind } from "../scanning/tokens";

export class FormattingService implements LanguageService {
public fillCapabilities(capabilities: ServerCapabilities): void {
capabilities.documentFormattingProvider = true;
}

public activate(connection: IConnection, documents: TextDocuments): void {
connection.onDocumentFormatting(params => {
const { uri } = params.textDocument;
const compilation = accessCache(documents, uri);

const { insertSpaces, tabSize } = params.options;
const indentation = insertSpaces ? " ".repeat(tabSize) : "\t";

const result = FormattingService.format(compilation, indentation);
const fullRange = Range.create(0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
return [TextEdit.replace(fullRange, result)];
});
}

public static format(compilation: Compilation, indentationValue: string): string {
const lines = Array<FormattingLine>();
let indentationLevel = 0;

for (const token of compilation.tokens) {
const lineIndex = token.range.start.line;
while (lines.length < lineIndex) {
lines.push(new FormattingLine(indentationLevel));
}

switch (token.kind) {
case TokenKind.RightCurlyBracket:
case TokenKind.RightSquareBracket: {
indentationLevel -= 1;
break;
}
}

if (lines.length === lineIndex) {
lines.push(new FormattingLine(indentationLevel));
}

lines[lineIndex].add(token);

switch (token.kind) {
case TokenKind.LeftCurlyBracket:
case TokenKind.LeftSquareBracket: {
indentationLevel += 1;
break;
}
}
}

lines.push(new FormattingLine(0));
return lines.map(line => line.format(indentationValue)).join(compilation.text.includes("\r\n") ? "\r\n" : "\n");
}
}

class FormattingLine {
private readonly tokens = Array<Token>();

public constructor(private readonly indentationLevel: number) {}

public add(token: Token): void {
this.tokens.push(token);
}

public format(indentationValue: string): string {
let result = indentationValue.repeat(this.indentationLevel);

function append(value: string): void {
if (value.trim() || result.trim()) {
result += value;
}
}

for (const token of this.tokens) {
switch (token.kind) {
case TokenKind.VersionKeyword:
case TokenKind.WorkflowKeyword:
case TokenKind.ActionKeyword:
case TokenKind.OnKeyword:
case TokenKind.ResolvesKeyword:
case TokenKind.UsesKeyword:
case TokenKind.NeedsKeyword:
case TokenKind.RunsKeyword:
case TokenKind.ArgsKeyword:
case TokenKind.EnvKeyword:
case TokenKind.SecretsKeyword:
case TokenKind.Equal:
case TokenKind.Identifier:
case TokenKind.IntegerLiteral:
case TokenKind.StringLiteral:
case TokenKind.Unrecognized:
case TokenKind.Comment:
case TokenKind.LeftCurlyBracket:
case TokenKind.LeftSquareBracket:
case TokenKind.RightCurlyBracket:
case TokenKind.RightSquareBracket: {
append(" ");
append(token.text);
break;
}
case TokenKind.Comma: {
append(token.text);
break;
}
default: {
throw new Error(`Unexpected token kind '${token.kind}'`);
}
}
}

return result.trimRight();
}
}
73 changes: 73 additions & 0 deletions test/formatting.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*!
* Copyright 2019 Omar Tawfik. Please see LICENSE file at the root of this repository.
*/

import { Compilation } from "../src/util/compilation";
import { FormattingService } from "../src/services/formatting";

describe(__filename, () => {
it("formats a one line empty action", () => {
expectFormatting(`
action"x"{}
`).toMatchInlineSnapshot(`
"
action \\"x\\" { }
"
`);
});

it("formats a one line action with properties", () => {
expectFormatting(`
action "x" { uses="./ci" }
`).toMatchInlineSnapshot(`
"
action \\"x\\" { uses = \\"./ci\\" }
"
`);
});

it("formats an action with embedded comments", () => {
expectFormatting(`
# on start of line
# should be aligned to start
action "x" {
# should be indented
uses="./ci" // should be at end
}
`).toMatchInlineSnapshot(`
"
# on start of line
# should be aligned to start
action \\"x\\" {
# should be indented
uses = \\"./ci\\" // should be at end
}
"
`);
});

it("formats a file with unrecognized characters", () => {
expectFormatting(`
# on start of line
# should be aligned to start
version =$
`).toMatchInlineSnapshot(`
"
# on start of line
# should be aligned to start
version = $
"
`);
});
});

function expectFormatting(text: string): jest.Matchers<string> {
const compilation = new Compilation(text);
const result = FormattingService.format(compilation, " ");

const secondCompilation = new Compilation(result);
const secondResult = FormattingService.format(secondCompilation, " ");
expect(result).toBe(secondResult);

return expect(result);
}

0 comments on commit b1092dc

Please sign in to comment.