Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ All notable changes will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- Issue #111: Added context menu option to prettify table at cursor without selection.
### Fixed
- Issue #106: Fixed table prettification breaking when cells contain zero-width characters.

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Makes tables more readable for humans. Compatible with the Markdown writer plugi

The extension is available for markdown language mode. It can either prettify a selection (`Format Selection`) or the entire document (`Format Document`).
A VSCode command called `Prettify markdown tables` is also available to format the currently opened document.
Right-click on a table to access the context menu option `Prettify markdown table at cursor` for formatting individual tables without selection.

### Configurable settings:
- The maximum texth length of a selection/entire document to consider for formatting. Default: 1M chars (limit does not apply from CLI or NPM).
Expand Down
15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
{
"command": "markdownTablePrettify.prettifyTables",
"title": "Prettify markdown tables"
},
{
"command": "markdownTablePrettify.prettifyTableAtCursor",
"title": "Prettify markdown table at cursor"
}
],
"keybindings": [
Expand All @@ -67,7 +71,16 @@
"mac": "cmd+alt+m",
"when": "editorTextFocus && !editorReadonly && !inCompositeEditor"
}
]
],
"menus": {
"editor/context": [
{
"command": "markdownTablePrettify.prettifyTableAtCursor",
"when": "markdownTablePrettify.hasTableAtCursor && editorTextFocus && !editorReadonly && !inCompositeEditor",
"group": "1_modification@2"
}
]
}
},
"capabilities": {
"documentFormattingProvider": "true"
Expand Down
33 changes: 29 additions & 4 deletions src/extension/extension.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';
import * as vscode from 'vscode';
import { getSupportLanguageIds, getDocumentRangePrettyfier, getDocumentPrettyfier, getDocumentPrettyfierCommand, invalidateCache } from './prettyfierFactory';
import { getSupportLanguageIds, getDocumentRangePrettyfier, getDocumentPrettyfier, getDocumentPrettyfierCommand, getTableAtCursorPrettyfier, invalidateCache } from './prettyfierFactory';
import { TableAtCursorContextKeyUpdater } from './tableAtCursorContextKeyUpdater';

// This method is called when the extension is activated.
// The extension is activated the very first time the command is executed.
Expand All @@ -23,11 +24,35 @@ export function activate(context: vscode.ExtensionContext): void {
);
}

const tableAtCursorContextKey = "markdownTablePrettify.hasTableAtCursor";
const contextKeyUpdater = new TableAtCursorContextKeyUpdater(
supportedLanguageIds,
tableAtCursorContextKey,
getTableAtCursorPrettyfier(),
vscode.commands.executeCommand
);

context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(editor => void contextKeyUpdater.update(editor)),
vscode.window.onDidChangeTextEditorSelection(event => void contextKeyUpdater.update(event.textEditor))
);

void contextKeyUpdater.update(vscode.window.activeTextEditor);

const command = "markdownTablePrettify.prettifyTables";
context.subscriptions.push(
vscode.commands.registerTextEditorCommand(command, textEditor => {
if (supportedLanguageIds.indexOf(textEditor.document.languageId) >= 0)
getDocumentPrettyfierCommand().prettifyDocument(textEditor);
vscode.commands.registerTextEditorCommand(command, async textEditor => {
if (supportedLanguageIds.includes(textEditor.document.languageId))
await getDocumentPrettyfierCommand().prettifyDocument(textEditor);
})
);

const formatTableCommand = "markdownTablePrettify.prettifyTableAtCursor";
context.subscriptions.push(
vscode.commands.registerTextEditorCommand(formatTableCommand, async textEditor => {
if (supportedLanguageIds.includes(textEditor.document.languageId)) {
await getTableAtCursorPrettyfier().prettifyTableAtCursor(textEditor);
}
})
);
}
Expand Down
20 changes: 20 additions & 0 deletions src/extension/prettyfierFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ import { SelectionInterpreter } from '../modelFactory/selectionInterpreter';
import { PadCalculatorSelector } from '../padCalculation/padCalculatorSelector';
import { AlignmentMarkerStrategy } from '../viewModelFactories/alignmentMarking';
import { MultiTablePrettyfier } from '../prettyfiers/multiTablePrettyfier';
import { TableAtCursorPrettyfier } from "../prettyfiers/tableAtCursorPrettyfier";
import { SingleTablePrettyfier } from '../prettyfiers/singleTablePrettyfier';
import { TableStringWriter } from "../writers/tableStringWriter";
import { ValuePaddingProvider } from '../writers/valuePaddingProvider';

let cachedMultiTablePrettyfier: MultiTablePrettyfier | null = null;
let cachedTableAtCursorPrettyfier: TableAtCursorPrettyfier | null = null;

export function invalidateCache() {
cachedMultiTablePrettyfier = null;
cachedTableAtCursorPrettyfier = null;
}

export function getSupportLanguageIds() {
Expand Down Expand Up @@ -64,6 +67,23 @@ function getMultiTablePrettyfier(): MultiTablePrettyfier {
return cachedMultiTablePrettyfier;
}

export function getTableAtCursorPrettyfier(): TableAtCursorPrettyfier {
if (cachedTableAtCursorPrettyfier) {
return cachedTableAtCursorPrettyfier;
}

const loggers = getLoggers();
const sizeLimitChecker = getSizeLimitChecker(loggers);
const columnPadding = getConfigurationValue<number>("columnPadding", 0);

cachedTableAtCursorPrettyfier = new TableAtCursorPrettyfier(
new TableFinder(new TableValidator(new SelectionInterpreter(true))),
getSingleTablePrettyfier(loggers, sizeLimitChecker, columnPadding)
);

return cachedTableAtCursorPrettyfier;
}

function getSingleTablePrettyfier(loggers: ILogger[], sizeLimitCheker: ConfigSizeLimitChecker, columnPadding: number): SingleTablePrettyfier {
return new SingleTablePrettyfier(
new TableFactory(
Expand Down
46 changes: 46 additions & 0 deletions src/extension/tableAtCursorContextKeyUpdater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as vscode from 'vscode';
import { Document } from '../models/doc/document';
import { TableAtCursorPrettyfier } from '../prettyfiers/tableAtCursorPrettyfier';

export class TableAtCursorContextKeyUpdater {
private _lastEditorUri: string | null = null;
private _lastDocumentVersion: number | null = null;
private _lastLine: number | null = null;

constructor(
private readonly _supportedLanguageIds: string[],
private readonly _contextKey: string,
private readonly _tableAtCursorPrettyfier: TableAtCursorPrettyfier,
private readonly _executeCommand: (command: string, ...args: any[]) => Thenable<unknown>
) { }

public async update(editor?: vscode.TextEditor): Promise<void> {
if (!editor || !this._supportedLanguageIds.includes(editor.document.languageId)) {
this._lastEditorUri = null;
this._lastDocumentVersion = null;
this._lastLine = null;
await this._executeCommand('setContext', this._contextKey, false);
return;
}

const currentEditorUri = editor.document.uri.toString();
const currentDocumentVersion = editor.document.version;
const currentLine = editor.selection.active.line;

if (this._lastEditorUri === currentEditorUri
&& this._lastDocumentVersion === currentDocumentVersion
&& this._lastLine === currentLine) {
return;
}

this._lastEditorUri = currentEditorUri;
this._lastDocumentVersion = currentDocumentVersion;
this._lastLine = currentLine;

const hasTableAtCursor = this._tableAtCursorPrettyfier.hasTableAtCursor(
new Document(editor.document.getText()),
currentLine);

await this._executeCommand('setContext', this._contextKey, hasTableAtCursor);
}
}
4 changes: 2 additions & 2 deletions src/extension/tableDocumentPrettyfierCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ export class TableDocumentPrettyfierCommand {
private readonly _multiTablePrettyfier: MultiTablePrettyfier
) { }

public prettifyDocument(editor: vscode.TextEditor) {
public async prettifyDocument(editor: vscode.TextEditor): Promise<void> {
const formattedDocument: string = this._multiTablePrettyfier.formatTables(editor.document.getText());

editor.edit(textEditorEdit => {
await editor.edit(textEditorEdit => {
textEditorEdit.replace(new vscode.Range(
new vscode.Position(0, 0),
new vscode.Position(editor.document.lineCount - 1, Number.MAX_SAFE_INTEGER)
Expand Down
45 changes: 45 additions & 0 deletions src/prettyfiers/tableAtCursorPrettyfier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as vscode from "vscode";
import { TableFinder } from "../tableFinding/tableFinder";
import { SingleTablePrettyfier } from "../prettyfiers/singleTablePrettyfier";
import { Document } from "../models/doc/document";
import { Range } from "../models/doc/range";

export class TableAtCursorPrettyfier {

constructor(
private readonly _tableFinder: TableFinder,
private readonly _singleTablePrettyfier: SingleTablePrettyfier
) { }

public async prettifyTableAtCursor(editor: vscode.TextEditor): Promise<boolean> {
const cursorLine = editor.selection.active.line;
const document = new Document(editor.document.getText());

const tableRange = this.findTableRangeAtLine(document, cursorLine);
if (tableRange == null) {
return false;
}

const formattedTable = this._singleTablePrettyfier.prettifyTable(document, tableRange);

await editor.edit(editBuilder => {
editBuilder.replace(
new vscode.Range(
new vscode.Position(tableRange.startLine, 0),
editor.document.lineAt(tableRange.endLine).range.end
),
formattedTable
);
});

return true;
}

public hasTableAtCursor(document: Document, cursorLine: number): boolean {
return this.findTableRangeAtLine(document, cursorLine) != null;
}

private findTableRangeAtLine(document: Document, cursorLine: number): Range | null {
return this._tableFinder.getRangeContainingLine(document, cursorLine);
}
}
73 changes: 68 additions & 5 deletions src/tableFinding/tableFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@ export class TableFinder {
}

if (!isInIgnoreBlock) {
const isValidSeparatorRow = this._tableValidator.lineIsValidSeparator(document.lines[rowIndex].value);
const nextRangeResult: { range: Range | null, ignoreBlockStarted: boolean } = isValidSeparatorRow
? this.getNextValidTableRange(document, rowIndex)
: { range: null, ignoreBlockStarted: isInIgnoreBlock};

const nextRangeResult = this.getRangeAtSeparatorRow(document, rowIndex);
isInIgnoreBlock = nextRangeResult.ignoreBlockStarted;

if (nextRangeResult.range != null) {
Expand All @@ -40,6 +36,73 @@ export class TableFinder {
return null;
}

public getRangeContainingLine(document: Document, lineIndex: number): Range | null {
if (lineIndex < 0 || lineIndex >= document.lines.length) {
return null;
}

// search locally around the cursor
const trySeparator = (separatorRowIndex: number): Range | null => {
if (this.isLineInsideIgnoreBlock(document, separatorRowIndex)) {
return null;
}

const range = this.getRangeAtSeparatorRow(document, separatorRowIndex).range;
return range != null && range.startLine <= lineIndex && lineIndex <= range.endLine
? range
: null;
};

const initialRange = trySeparator(lineIndex);
if (initialRange != null) {
return initialRange;
}

let offset = 1;
while (lineIndex - offset >= 0 || lineIndex + offset < document.lines.length) {
if (lineIndex - offset >= 0) {
const range = trySeparator(lineIndex - offset);
if (range != null) {
return range;
}
}

if (lineIndex + offset < document.lines.length) {
const range = trySeparator(lineIndex + offset);
if (range != null) {
return range;
}
}

offset++;
}

return null;
}

private isLineInsideIgnoreBlock(document: Document, lineIndex: number): boolean {
let ignoreBlockStarted = false;

for (let index = 0; index <= lineIndex && index < document.lines.length; index++) {
const trimmedLine = document.lines[index].value.trim();
if (trimmedLine == this._ignoreStart) {
ignoreBlockStarted = true;
} else if (trimmedLine == this._ignoreEnd) {
ignoreBlockStarted = false;
}
}

return ignoreBlockStarted;
}

private getRangeAtSeparatorRow(document: Document, separatorRowIndex: number): { range: Range | null, ignoreBlockStarted: boolean } {
if (!this._tableValidator.lineIsValidSeparator(document.lines[separatorRowIndex].value)) {
return { range: null, ignoreBlockStarted: false };
}

return this.getNextValidTableRange(document, separatorRowIndex);
}

private getNextValidTableRange(document: Document, separatorRowIndex: number): { range: Range | null, ignoreBlockStarted: boolean} {
let firstTableFileRow = separatorRowIndex - 1;
let lastTableFileRow = separatorRowIndex;
Expand Down
Loading
Loading