From dece59b3e9e4b1fafbce2b0e2a3ca11e2755802f Mon Sep 17 00:00:00 2001 From: Roman Peshkov Date: Tue, 13 Mar 2018 00:42:33 +0100 Subject: [PATCH] Initial commit --- .gitignore | 6 ++ .vscode/extensions.json | 7 ++ .vscode/launch.json | 36 +++++++++++ .vscode/settings.json | 9 +++ .vscode/tasks.json | 20 ++++++ .vscodeignore | 9 +++ CHANGELOG.md | 9 +++ LICENSE | 21 ++++++ README.md | 11 ++++ package.json | 55 ++++++++++++++++ src/extension.ts | 52 +++++++++++++++ src/test/extension.test.ts | 22 +++++++ src/test/index.ts | 22 +++++++ src/ttMarkdown.ts | 128 +++++++++++++++++++++++++++++++++++++ src/ttOrg.ts | 108 +++++++++++++++++++++++++++++++ src/ttTable.ts | 89 ++++++++++++++++++++++++++ tsconfig.json | 23 +++++++ tslint.json | 15 +++++ 18 files changed, 642 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 .vscodeignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 package.json create mode 100644 src/extension.ts create mode 100644 src/test/extension.test.ts create mode 100644 src/test/index.ts create mode 100644 src/ttMarkdown.ts create mode 100644 src/ttOrg.ts create mode 100644 src/ttTable.ts create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec36452 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +out +node_modules +.vscode-test/ +*.vsix + +.DS_Store diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ee71911 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "eg2.tslint" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c174db3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,36 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "npm: watch" + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "outFiles": [ + "${workspaceFolder}/out/test/**/*.js" + ], + "preLaunchTask": "npm: watch" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d137133 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + } +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..604e38f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,20 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..8557178 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,9 @@ +.vscode/** +.vscode-test/** +out/test/** +out/**/*.map +src/** +.gitignore +tsconfig.json +vsc-extension-quickstart.md +tslint.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8bf33b8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log + +All notable changes to the "vscode-text-tables" extension will be documented in this file. + +Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. + +## [Unreleased] + +- Initial release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0a39310 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Roman Peshkov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3bda333 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Text Tables + +VSCode extension that brings the power of Emacs table editing. + +Work in progress. Not in marketplace. There's not so much to see yet. + +## [License](LICENSE) + +The MIT License (MIT) + +Copyright (c) 2018 Roman Peshkov diff --git a/package.json b/package.json new file mode 100644 index 0000000..cbc1e58 --- /dev/null +++ b/package.json @@ -0,0 +1,55 @@ +{ + "name": "vscode-text-tables", + "displayName": "Text Tables", + "description": "Work with text tables without pain", + "version": "0.0.1", + "publisher": "RomanPeshkov", + "engines": { + "vscode": "^1.21.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onCommand:text-tables.formatUnderCursor" + ], + "main": "./out/extension", + "contributes": { + "commands": [ + { + "command": "text-tables.formatUnderCursor", + "title": "Text Tables: Format under cursor" + } + ], + "configuration": { + "type": "object", + "title": "Text Tables configuration", + "properties": { + "text-tables.mode": { + "type": "string", + "enum": [ + "markdown", + "org" + ], + "default": "markdown", + "description": "Sets the mode in which extension should work", + "scope": "window" + } + } + } + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "postinstall": "node ./node_modules/vscode/bin/install", + "test": "npm run compile && node ./node_modules/vscode/bin/test" + }, + "devDependencies": { + "typescript": "^2.6.1", + "vscode": "^1.1.6", + "tslint": "^5.8.0", + "@types/node": "^7.0.43", + "@types/mocha": "^2.2.42" + } +} diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..85598ca --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,52 @@ +'use strict'; + +import * as vscode from 'vscode'; +import { OrgLocator, OrgParser, OrgStringifier } from './ttOrg'; +import { Locator, Parser, Stringifier } from './ttTable'; +import { MarkdownLocator, MarkdownParser, MarkdownStringifier } from './ttMarkdown'; + +export function activate(ctx: vscode.ExtensionContext) { + ctx.subscriptions.push(vscode.commands.registerCommand('text-tables.formatUnderCursor', () => { + const config = vscode.workspace.getConfiguration('text-tables'); + const mode = config.get('mode', ''); + + let locator: Locator; + let parser: Parser; + let stringifier: Stringifier; + + if (mode === 'org') { + locator = new OrgLocator(); + parser = new OrgParser(); + stringifier = new OrgStringifier(); + } else { + locator = new MarkdownLocator(); + parser = new MarkdownParser(); + stringifier = new MarkdownStringifier(); + } + + + if (vscode.window.activeTextEditor !== undefined) { + const editor = vscode.window.activeTextEditor; + + const selectedRange = locator.locate(editor.document, editor.selection.start.line); + if (selectedRange !== undefined) { + const selectedText = editor.document.getText(selectedRange); + + const table = parser.parse(selectedText); + if (table !== undefined) { + const newText = stringifier.stringify(table); + + editor.edit(b => { + b.replace(selectedRange, newText); + }); + } + + } + } + + //vscode.window.showInformationMessage(mode); + })); +} + +export function deactivate() { +} diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts new file mode 100644 index 0000000..a7a297f --- /dev/null +++ b/src/test/extension.test.ts @@ -0,0 +1,22 @@ +// +// Note: This example test is leveraging the Mocha test framework. +// Please refer to their documentation on https://mochajs.org/ for help. +// + +// The module 'assert' provides assertion methods from node +import * as assert from 'assert'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +// import * as vscode from 'vscode'; +// import * as myExtension from '../extension'; + +// Defines a Mocha test suite to group tests of similar kind together +suite("Extension Tests", function () { + + // Defines a Mocha unit test + test("Something 1", function() { + assert.equal(-1, [1, 2, 3].indexOf(5)); + assert.equal(-1, [1, 2, 3].indexOf(0)); + }); +}); \ No newline at end of file diff --git a/src/test/index.ts b/src/test/index.ts new file mode 100644 index 0000000..9fa2ea0 --- /dev/null +++ b/src/test/index.ts @@ -0,0 +1,22 @@ +// +// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING +// +// This file is providing the test runner to use when running extension tests. +// By default the test runner in use is Mocha based. +// +// You can provide your own test runner if you want to override it by exporting +// a function run(testRoot: string, clb: (error:Error) => void) that the extension +// host can call to run the tests. The test runner is expected to use console.log +// to report the results back to the caller. When the tests are finished, return +// a possible error to the callback or null if none. + +import * as testRunner from 'vscode/lib/testrunner'; + +// You can directly control Mocha options by uncommenting the following lines +// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info +testRunner.configure({ + ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) + useColors: true // colored output from test results +}); + +module.exports = testRunner; \ No newline at end of file diff --git a/src/ttMarkdown.ts b/src/ttMarkdown.ts new file mode 100644 index 0000000..3ec55bc --- /dev/null +++ b/src/ttMarkdown.ts @@ -0,0 +1,128 @@ +import * as tt from './ttTable'; +import * as vscode from 'vscode'; + +const verticalSeparator = '|'; +const horizontalSeparator = '-'; +const intersection = '|'; + +export class MarkdownParser implements tt.Parser { + parse(text: string): tt.Table | undefined { + if (!text || text.length === 0) { + return undefined; + } + + const result = new tt.Table(); + + const strings = text.split('\n'); + + for (let s of strings) { + s = s.trim(); + + if (!s.startsWith(verticalSeparator)) { + continue; + } + + const cleanedString = s.replace(/\s+/g, ''); + if (cleanedString.startsWith('|-') || cleanedString.startsWith('|:-')) { + result.addRow(tt.RowType.Separator, []); + for (let part of cleanedString.split('|')) { + if (part.length < 3) { + continue; + } + let trimmed = part.trim(); + let align = tt.Alignment.Left; + if (trimmed[trimmed.length - 1] === ':') { + if (trimmed[0] === ':') { + align = tt.Alignment.Center; + } else { + align = tt.Alignment.Right; + } + } + + result.cols.push({alignment: align, width: 3}); + } + continue; + } + + let lastIndex = s.length; + if (s.endsWith(verticalSeparator)) { + lastIndex--; + } + + const values = s + .slice(1, lastIndex) + .split(verticalSeparator) + .map(x => x.trim()); + + result.addRow(tt.RowType.Data, values); + } + + return result; + } +} + +export class MarkdownStringifier implements tt.Stringifier { + stringify(table: tt.Table): string { + table.normalize(); + table.calculateColDefs(); + + const result = []; + + for (let i = 0; i < table.rows.length; ++i) { + let rowString = ''; + const rowData = table.getRow(i); + if (table.rows[i].type === tt.RowType.Data) { + rowString = rowData.reduce((prev, cur, idx) => { + const pad = ' '.repeat(table.cols[idx].width - cur.length + 1); + return prev + ' ' + cur + pad + verticalSeparator; + }, verticalSeparator); + } else { + rowString = rowData.reduce((prev, _, idx) => { + const begin = table.cols[idx].alignment === tt.Alignment.Center + ? ' :' + : ' -'; + const ending = table.cols[idx].alignment !== tt.Alignment.Left + ? ': ' + verticalSeparator + : '- ' + verticalSeparator; + return prev + begin + horizontalSeparator.repeat(table.cols[idx].width-2) + ending; + }, verticalSeparator); + } + + result.push(rowString); + } + + return result.join('\n'); + } +} + +export class MarkdownLocator implements tt.Locator { + locate(reader: tt.LineReader, lineNr: number): vscode.Range | undefined { + const isTableLikeString = (lineNr: number) => { + if (lineNr < 0 || lineNr >= reader.lineCount) { + return false; + } + const firstCharIdx = reader.lineAt(lineNr).firstNonWhitespaceCharacterIndex; + const firstChar = reader.lineAt(lineNr).text[firstCharIdx]; + return firstChar === '|'; + }; + + let start = lineNr; + while (isTableLikeString(start)) { + start--; + } + + let end = lineNr; + while (isTableLikeString(end)) { + end++; + } + + if (start === end) { + return undefined; + } + + const startPos = reader.lineAt(start + 1).range.start; + const endPos = reader.lineAt(end - 1).range.end; + + return new vscode.Range(startPos, endPos); + } +} diff --git a/src/ttOrg.ts b/src/ttOrg.ts new file mode 100644 index 0000000..6d92ab4 --- /dev/null +++ b/src/ttOrg.ts @@ -0,0 +1,108 @@ +import * as tt from './ttTable'; +import * as vscode from 'vscode'; + +const verticalSeparator = '|'; +const horizontalSeparator = '-'; +const intersection = '+'; + +export class OrgParser implements tt.Parser { + parse(text: string): tt.Table | undefined { + if (!text || text.length === 0) { + return undefined; + } + + const result = new tt.Table(); + + const strings = text.split('\n'); + + for (let s of strings) { + s = s.trim(); + + if (!s.startsWith(verticalSeparator)) { + continue; + } + + if (s.length > 1 && s[1] === horizontalSeparator) { + result.addRow(tt.RowType.Separator, []); + continue; + } + + let lastIndex = s.length; + if (s.endsWith(verticalSeparator)) { + lastIndex--; + } + + const values = s + .slice(1, lastIndex) + .split(verticalSeparator) + .map(x => x.trim()); + + result.addRow(tt.RowType.Data, values); + } + + return result; + } +} + +export class OrgStringifier implements tt.Stringifier { + stringify(table: tt.Table): string { + table.normalize(); + table.calculateColDefs(); + + const result = []; + + for (let i = 0; i < table.rows.length; ++i) { + let rowString = ''; + const rowData = table.getRow(i); + if (table.rows[i].type === tt.RowType.Data) { + rowString = rowData.reduce((prev, cur, idx) => { + const pad = ' '.repeat(table.cols[idx].width - cur.length + 1); + return prev + ' ' + cur + pad + verticalSeparator; + }, verticalSeparator); + } else { + rowString = rowData.reduce((prev, _, idx) => { + const ending = (idx === table.cols.length - 1) + ? verticalSeparator + : intersection; + return prev + horizontalSeparator.repeat(table.cols[idx].width + 2) + ending; + }, verticalSeparator); + } + + result.push(rowString); + } + + return result.join('\n'); + } +} + +export class OrgLocator implements tt.Locator { + locate(reader: tt.LineReader, lineNr: number): vscode.Range | undefined { + const isTableLikeString = (lineNr: number) => { + if (lineNr < 0 || lineNr >= reader.lineCount) { + return false; + } + const firstCharIdx = reader.lineAt(lineNr).firstNonWhitespaceCharacterIndex; + const firstChar = reader.lineAt(lineNr).text[firstCharIdx]; + return firstChar === '|'; + }; + + let start = lineNr; + while (isTableLikeString(start)) { + start--; + } + + let end = lineNr; + while (isTableLikeString(end)) { + end++; + } + + if (start === end) { + return undefined; + } + + const startPos = reader.lineAt(start + 1).range.start; + const endPos = reader.lineAt(end - 1).range.end; + + return new vscode.Range(startPos, endPos); + } +} diff --git a/src/ttTable.ts b/src/ttTable.ts new file mode 100644 index 0000000..a563f06 --- /dev/null +++ b/src/ttTable.ts @@ -0,0 +1,89 @@ +import * as vscode from 'vscode'; + +export enum RowType { + Unknown, + Separator, + Data +} + +export enum Alignment { + Left, + Center, + Right +} + +export interface RowDef { + type: RowType; +} + +export interface ColDef { + alignment: Alignment; + width: number; +} + +export class Table { + rows: RowDef[] = []; + cols: ColDef[] = []; + + private data: string[][] = []; + + addRow(type: RowType, values: string[]) { + this.rows.push({type}); + this.data.push(values); + } + + getAt(row: number, col: number): string { + return this.data[row][col]; + } + + getRow(row: number): string[] { + return this.data[row]; + } + + setAt(row: number, col: number, value: string) { + this.data[row][col] = value; + } + + normalize() { + const maxColumns = Math.max(...this.data.map(x => x.length)); + + for (const row of this.data) { + while (row.length < maxColumns) { + row.push(''); + } + } + } + + calculateColDefs() { + const colCount = this.data[0].length; + const adjustCount = colCount - this.cols.length; + for (let i = 0; i < adjustCount; ++i) { + this.cols.push({ alignment: Alignment.Left, width: 0 }); + } + + for (let row = 0; row < this.data.length; ++row) { + for (let col = 0; col < this.data[row].length; ++col) { + if (this.data[row][col].length > this.cols[col].width) { + this.cols[col].width = this.data[row][col].length; + } + } + } + } +} + +export interface Parser { + parse(text: string): Table | undefined; +} + +export interface Stringifier { + stringify(table: Table): string; +} + +export interface Locator { + locate(reader: LineReader, lineNr: number): vscode.Range | undefined; +} + +export interface LineReader { + lineAt(line: number): vscode.TextLine; + lineCount: number; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..de3ce5d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "lib": [ + "es6" + ], + "sourceMap": true, + "rootDir": "src", + /* Strict Type-Checking Option */ + "strict": true, /* enable all strict type-checking options */ + /* Additional Checks */ + "noUnusedLocals": true, /* Report errors on unused locals. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + "noUnusedParameters": true, /* Report errors on unused parameters. */ + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..2bd680d --- /dev/null +++ b/tslint.json @@ -0,0 +1,15 @@ +{ + "rules": { + "no-string-throw": true, + "no-unused-expression": true, + "no-duplicate-variable": true, + "curly": true, + "class-name": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": true + }, + "defaultSeverity": "warning" +} \ No newline at end of file