├── .gitignore ├── icon.png ├── icon.drawio.png ├── images ├── insert.gif ├── align_column.gif ├── csv_to_table.gif ├── format_table.gif ├── keep_indent.gif ├── keep_indent_before.gif ├── navigate_next_cell.gif ├── navigate_prev_cell.gif ├── table_from_excel.gif ├── move_columns_at_once.gif └── align_columns_at_once.gif ├── .vscodeignore ├── .vscode ├── extensions.json ├── tasks.json ├── settings.json └── launch.json ├── src ├── test │ ├── sample │ │ ├── demo.txt │ │ ├── demo.md │ │ ├── test.txt │ │ ├── demo2.md │ │ ├── tsv.md │ │ ├── table.md │ │ ├── table.mdx │ │ ├── table.qmd │ │ └── issue.md │ └── .vscode │ │ └── settings.json ├── definitions │ └── supportLanguage.ts ├── markdownTableData.ts ├── doOnSaveServices │ └── formatOnSaveService.ts ├── contextServices │ ├── workspaceBooleanConfigurationContextService.ts │ ├── contextService.ts │ ├── textEditorContextServiceIsSupportedLanguage.ts │ ├── textEditorContextServiceCursorInTable.ts │ └── contextServiceManager.ts ├── textUtility.ts ├── extension.ts ├── markdownTableUtility.ts ├── markdownTableDataHelper.ts └── commands.ts ├── .eslintrc.json ├── tsconfig.json ├── CHANGELOG.md ├── README.md ├── LICENSE └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takumisoft68/vscode-markdown-table/HEAD/icon.png -------------------------------------------------------------------------------- /icon.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takumisoft68/vscode-markdown-table/HEAD/icon.drawio.png -------------------------------------------------------------------------------- /images/insert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takumisoft68/vscode-markdown-table/HEAD/images/insert.gif -------------------------------------------------------------------------------- /images/align_column.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takumisoft68/vscode-markdown-table/HEAD/images/align_column.gif -------------------------------------------------------------------------------- /images/csv_to_table.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takumisoft68/vscode-markdown-table/HEAD/images/csv_to_table.gif -------------------------------------------------------------------------------- /images/format_table.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takumisoft68/vscode-markdown-table/HEAD/images/format_table.gif -------------------------------------------------------------------------------- /images/keep_indent.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takumisoft68/vscode-markdown-table/HEAD/images/keep_indent.gif -------------------------------------------------------------------------------- /images/keep_indent_before.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takumisoft68/vscode-markdown-table/HEAD/images/keep_indent_before.gif -------------------------------------------------------------------------------- /images/navigate_next_cell.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takumisoft68/vscode-markdown-table/HEAD/images/navigate_next_cell.gif -------------------------------------------------------------------------------- /images/navigate_prev_cell.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takumisoft68/vscode-markdown-table/HEAD/images/navigate_prev_cell.gif -------------------------------------------------------------------------------- /images/table_from_excel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takumisoft68/vscode-markdown-table/HEAD/images/table_from_excel.gif -------------------------------------------------------------------------------- /images/move_columns_at_once.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takumisoft68/vscode-markdown-table/HEAD/images/move_columns_at_once.gif -------------------------------------------------------------------------------- /images/align_columns_at_once.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takumisoft68/vscode-markdown-table/HEAD/images/align_columns_at_once.gif -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/test/sample/demo.txt: -------------------------------------------------------------------------------- 1 | # title 2 | 3 | | column A | column B | column C | 4 | | :------- | :------- | :------- | 5 | | data | data | data | 6 | | data | | | 7 | | | | | 8 | -------------------------------------------------------------------------------- /src/test/sample/demo.md: -------------------------------------------------------------------------------- 1 | # title 2 | 3 | | column A | column B | column C | 4 | | :------- | :------- | :------- | 5 | | data | data | data | 6 | | data | | | 7 | | | | | 8 | -------------------------------------------------------------------------------- /src/test/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdownlint.config": { 3 | "MD012": false, 4 | 5 | }, 6 | "markdowntable.showMenu.csvToTable": true, 7 | "markdowntable.ignoreCodeblock": true, 8 | "markdowntable.showMenu.moveLeft": true 9 | } -------------------------------------------------------------------------------- /src/test/sample/test.txt: -------------------------------------------------------------------------------- 1 | | column A | column B | column C | column D | 2 | | :-- | :--: | --: | :-- | 3 | | data A1 | data B1 | data C1 | data D1 | 4 | | data A2 data A2 | data B2 | data C2 | data D2 | 5 | | data A3 | data B3 data B3 | data C3 | data D3 | 6 | | data A4 | data B4 | data C4 | data D4 | -------------------------------------------------------------------------------- /src/definitions/supportLanguage.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument } from 'vscode'; 2 | 3 | export class SupportLanguage { 4 | public static isSupportedLanguage(document: TextDocument | undefined): boolean { 5 | if (!document || document.languageId === null) { 6 | return false; 7 | } 8 | return (document.languageId === 'markdown' || document.languageId === 'mdx' || document.languageId === 'quarto'); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/semi": "warn", 13 | "curly": "warn", 14 | "eqeqeq": "warn", 15 | "no-throw-literal": "warn", 16 | "semi": "off" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /src/test/sample/demo2.md: -------------------------------------------------------------------------------- 1 | # title 2 | 3 | | column A | column B | column C | column D | 4 | | :-------------- | :-------------- | :-------------: | --------------: | 5 | | data A1 | data B1 | data C1 | data D1 | 6 | | data A2 data A2 | data B2 | data C2 | data D2 | 7 | | data A3 | data B3 data B3 | data C3 | data D3 | 8 | | data A4 | data B4 | data C4 data C4 | data D4 | 9 | | data A4 | data B4 | data C4 | data D4 data D4 | 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "preLaunchTask": "${defaultBuildTask}", 20 | "env": { 21 | "VSCODE_DEBUG_MODE": "true" 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/markdownTableData.ts: -------------------------------------------------------------------------------- 1 | 2 | export default class MarkdownTableData { 3 | public readonly originalText: string; 4 | public readonly aligns: [string, string][]; 5 | public readonly alignTexts: string[]; 6 | public readonly columns: string[]; 7 | public readonly cells: string[][]; 8 | public readonly leftovers: string[]; 9 | public readonly indent: string; 10 | 11 | constructor(_text: string, _aligns: [string, string][], _alignTexts: string[], _columns: string[], _cells: string[][], _leftovers: string[], _indent: string) { 12 | this.originalText = _text; 13 | this.aligns = _aligns; 14 | this.alignTexts = _alignTexts; 15 | this.columns = _columns; 16 | this.cells = _cells; 17 | this.leftovers = _leftovers; 18 | this.indent = _indent; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/doOnSaveServices/formatOnSaveService.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, window, Disposable, TextDocumentWillSaveEvent, commands, workspace } from 'vscode'; 2 | import { SupportLanguage } from '../definitions/supportLanguage'; 3 | 4 | export class FormatOnSaveService implements Disposable { 5 | 6 | public constructor() { } 7 | 8 | public activate(context: ExtensionContext) { 9 | context.subscriptions.push( 10 | workspace.onWillSaveTextDocument((event) => this.onWillSaveTextDocument(event)) 11 | ); 12 | } 13 | 14 | public dispose(): void { } 15 | 16 | private onWillSaveTextDocument(event: TextDocumentWillSaveEvent): void { 17 | let isSupportedLanguage = SupportLanguage.isSupportedLanguage(event.document); 18 | if (!isSupportedLanguage) { 19 | return; 20 | } 21 | 22 | let formatOnSave = workspace.getConfiguration("markdowntable").get("formatOnSave"); 23 | if (!formatOnSave) { 24 | return; 25 | } 26 | 27 | commands.executeCommand("markdowntable.format"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/sample/tsv.md: -------------------------------------------------------------------------------- 1 | | column A | column B | column C | column D | 2 | | :-------------- | :-------------- | :-------------: | --------------: | 3 | | data A1 | data B1 | data C1 | data D1 | 4 | | data A2 data A2 | data B2 | data C2 | data D2 | 5 | | data A3 | data B3 data B3 | data C3 | data D3 | 6 | | data A4 | data B4 | data C4 data C4 | data D4 | 7 | | data A4 | data B4 | data C4 | data D4 data D4 | 8 | 9 | 10 | columnA columnB columnC columnD 11 | dataA1 dataB1 dataC1 dataD1 over over 12 | dataA2dataA2 dataB2 dataC2 dataD2 over 13 | dataA3 dataB3dataB3 dataC3 dataD3 14 | dataA4 dataB4 dataC4 dataD4 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | columnA,columnB,columnC,columnD 27 | dataA1,dataB1,dataC1,dataD1,over,over 28 | dataA2dataA2,dataB2,dataC2,dataD2,over 29 | dataA3,dataB3dataB3,dataC3,dataD3 30 | dataA4,dataB4,dataC4 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | columnA columnB columnC columnD 57 | dataA1 dataB1 dataC1 dataD1 over over 58 | dataA2dataA2 dataB2 dataC2 dataD2 over 59 | dataA3 dataB3dataB3 dataC3 dataD3 60 | dataA4 dataB4 dataC4 61 | 62 | 63 | columnA,columnB,columnC,columnD 64 | dataA1,dataB1,dataC1,dataD1,over,over 65 | dataA2dataA2,dataB2,dataC2,dataD2,over 66 | dataA3,dataB3dataB3,dataC3,dataD3 67 | dataA4,dataB4,dataC4,dataD4 68 | 69 | -------------------------------------------------------------------------------- /src/contextServices/workspaceBooleanConfigurationContextService.ts: -------------------------------------------------------------------------------- 1 | import { ConfigurationChangeEvent, commands, ExtensionContext, workspace} from 'vscode'; 2 | import { ContextService } from "./contextService"; 3 | 4 | export class WorkspaceBooleanConfigurationContextService extends ContextService { 5 | private readonly configSectionDir: string; 6 | private readonly configSectionName: string; 7 | 8 | constructor(contextName: string, configSection: String) { 9 | super(contextName); 10 | this.configSectionDir = configSection.split('.').slice(0, -1).join('.'); 11 | this.configSectionName = configSection.split('.').slice(-1)[0]; 12 | } 13 | 14 | public onActivate(context: ExtensionContext) { 15 | super.onActivate(context); 16 | 17 | // set initial state of context 18 | let showMenuInsertToc = workspace.getConfiguration(this.configSectionDir).get(this.configSectionName); 19 | this.setState(showMenuInsertToc); 20 | } 21 | 22 | public onDidChangeConfiguration(event: ConfigurationChangeEvent): void { 23 | super.onDidChangeConfiguration(event); 24 | if (event.affectsConfiguration(this.configSectionDir + "." + this.configSectionName)) { 25 | this.updateContextState(); 26 | } 27 | return; 28 | } 29 | 30 | private updateContextState() { 31 | let showMenuInsertToc = workspace.getConfiguration(this.configSectionDir).get(this.configSectionName); 32 | // console.debug("set " + this.contextName + " to " + showMenuInsertToc); 33 | this.setState(showMenuInsertToc); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/contextServices/contextService.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, commands, ExtensionContext, ConfigurationChangeEvent, TextEditor, TextEditorSelectionChangeEvent } from 'vscode'; 2 | 3 | export class ContextService implements Disposable { 4 | constructor(contextName: string) { 5 | this.contextName = contextName; 6 | } 7 | 8 | protected readonly contextName: string; 9 | 10 | /** 11 | * activate context service 12 | * @param context ExtensionContext 13 | */ 14 | public onActivate(context: ExtensionContext): void { } 15 | 16 | /** 17 | * Dispose this object. 18 | * Override this method to dispose objects those the extend class has. 19 | */ 20 | public dispose(): void { } 21 | 22 | /** 23 | * Default handler of onDidChangeActiveTextEditor, do nothing. 24 | * Override this method to handle that event to update context state. 25 | */ 26 | public onDidChangeActiveTextEditor(editor: TextEditor | undefined): void { } 27 | 28 | /** 29 | * default handler of onDidChangeTextEditorSelection, do nothing. 30 | * Override this method to handle that event to update context state. 31 | */ 32 | public onDidChangeTextEditorSelection(event: TextEditorSelectionChangeEvent): void { } 33 | 34 | /** 35 | * Default handler of an event that is emitted when the {@link WorkspaceConfiguration configuration} changed. 36 | * Override this method to handle that event to update context state. 37 | */ 38 | public onDidChangeConfiguration(event: ConfigurationChangeEvent): void { } 39 | 40 | /** 41 | * set state of context 42 | */ 43 | protected setState(state: any) { 44 | commands.executeCommand('setContext', this.contextName, state); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/sample/table.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | | column A | column B \| `|` | | column C | `\\` | column D | | 4 | | :------- | :-------------- | :----- | :------------- | :--- | ----------: | --- | 5 | | data A1 | data B1 | | data C1 | | \| data D1 | | 6 | | data A2 | data B2 `\\` | `\A\\` | \`\\`` data C2 | | `|`data D2 | | 7 | | data A3 | data B3 | | data C3 | | `|` data D3 | | 8 | | data A4 | data B4 | | data C4 | | `data| D4` | | 9 | 10 | 11 | | column A | column B | column C | column D | 12 | | :-- | :--: | --: | :-- | 13 | | data A1 | data B1 | data C1 | data D1 | 14 | | data A2 data A2 | data B2 | data C2 | data D2 | 15 | | data A3 | data B3 data B3 | data C3 | data D3 | 16 | | data A4 | data B4 | data C4 | data D4 | 17 | 18 | 19 | | column A | column B | column C | column D | 20 | | :-------------- | :-------------- | :-------------: | --------------: | 21 | | data A1 | data B1 | data C1 | data D1 | 22 | | data A2 data A2 | data B2 | data C2 | data D2 | 23 | | data A3 | data B3 data B3 | data C3 | data D3 | 24 | | data A4 | data B4 | data C4 data C4 | data D4 | 25 | | data A4 | data B4 | data C4 | data D4 data D4 | 26 | 27 | 28 | | column A | column B | column C | column D | 29 | | :-- | :-- | :-- | :-- | 30 | | data A1 | data B1 | data C1 | data D1 | 31 | | data A2 data A2 | data B2 | data C2 | data D2 | 32 | | data A3 | data B3 data B3 | data C3 | data D3 | 33 | | data A4 | data B4 | data C4 | data D4 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/test/sample/table.mdx: -------------------------------------------------------------------------------- 1 | # 2 | 3 | | column A | column B \| `|` | | column C | `\\` | column D | | 4 | | :------- | :-------------- | :----- | :------------- | :--- | ----------: | --- | 5 | | data A1 | data B1 | | data C1 | | \| data D1 | | 6 | | data A2 | data B2 `\\` | `\A\\` | \`\\`` data C2 | | `|`data D2 | | 7 | | data A3 | data B3 | | data C3 | | `|` data D3 | | 8 | | data A4 | data B4 | | data C4 | | `data| D4` | | 9 | 10 | 11 | | column A | column B | column C | column D | 12 | | :-- | :--: | --: | :-- | 13 | | data A1 | data B1 | data C1 | data D1 | 14 | | data A2 data A2 | data B2 | data C2 | data D2 | 15 | | data A3 | data B3 data B3 | data C3 | data D3 | 16 | | data A4 | data B4 | data C4 | data D4 | 17 | 18 | 19 | | column A | column B | column C | column D | 20 | | :-------------- | :-------------: | :-------------: | :-------------: | 21 | | data A1 | data B1 | data C1 | data D1 | 22 | | data A2 data A2 | data B2 | data C2 | data D2 | 23 | | data A3 | data B3 data B3 | data C3 | data D3 | 24 | | data A4 | data B4 | data C4 data C4 | data D4 | 25 | | data A4 | data B4 | data C4 | data D4 data D4 | 26 | 27 | 28 | | column A | column B | column C | column D | 29 | | :-- | :-- | :-- | :-- | 30 | | data A1 | data B1 | data C1 | data D1 | 31 | | data A2 data A2 | data B2 | data C2 | data D2 | 32 | | data A3 | data B3 data B3 | data C3 | data D3 | 33 | | data A4 | data B4 | data C4 | data D4 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/test/sample/table.qmd: -------------------------------------------------------------------------------- 1 | # 2 | 3 | | column A | column B \| `|` | | column C | `\\` | column D | | 4 | | :------- | :-------------- | :----- | :------------- | :--- | ----------: | --- | 5 | | data A1 | data B1 | | data C1 | | \| data D1 | | 6 | | data A2 | data B2 `\\` | `\A\\` | \`\\`` data C2 | | `|`data D2 | | 7 | | data A3 | data B3 | | data C3 | | `|` data D3 | | 8 | | data A4 | data B4 | | data C4 | | `data| D4` | | 9 | 10 | 11 | | column A | column B | column C | column D | 12 | | :-- | :--: | --: | :-- | 13 | | data A1 | data B1 | data C1 | data D1 | 14 | | data A2 data A2 | data B2 | data C2 | data D2 | 15 | | data A3 | data B3 data B3 | data C3 | data D3 | 16 | | data A4 | data B4 | data C4 | data D4 | 17 | 18 | 19 | | column A | column B | column C | column D | 20 | | :-------------- | :-------------: | :-------------: | :-------------: | 21 | | data A1 | data B1 | data C1 | data D1 | 22 | | data A2 data A2 | data B2 | data C2 | data D2 | 23 | | data A3 | data B3 data B3 | data C3 | data D3 | 24 | | data A4 | data B4 | data C4 data C4 | data D4 | 25 | | data A4 | data B4 | data C4 | data D4 data D4 | 26 | 27 | 28 | | column A | column B | column C | column D | 29 | | :-- | :-- | :-- | :-- | 30 | | data A1 | data B1 | data C1 | data D1 | 31 | | data A2 data A2 | data B2 | data C2 | data D2 | 32 | | data A3 | data B3 data B3 | data C3 | data D3 | 33 | | data A4 | data B4 | data C4 | data D4 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/textUtility.ts: -------------------------------------------------------------------------------- 1 | import { workspace } from "vscode"; 2 | 3 | export function isInTable(text: string, startline: number, endline: number): boolean { 4 | const lines = text.split(/\r\n|\n|\r/); 5 | 6 | // 指定範囲がテキストに存在しない 7 | if (startline < 0 || lines.length <= startline || lines.length <= endline) { 8 | return false; 9 | } 10 | 11 | const ignoreCodeblock = workspace.getConfiguration('markdowntable').get('ignoreCodeblock'); 12 | if (ignoreCodeblock) { 13 | // 開始行がコードブロック内だったらTable内ではない 14 | let isInCodeBlock = false; 15 | for (let i = 0; i <= startline; i++) { 16 | if (lines[i].trim().startsWith("```")) { 17 | isInCodeBlock = !isInCodeBlock; 18 | } 19 | } 20 | if (isInCodeBlock) { 21 | return false; 22 | } 23 | 24 | // 開始行から終了行までの間にコードブロックを含む場合はTableではない 25 | let includeCodeBlock = false; 26 | for (let i = startline; i <= endline; i++) { 27 | if (lines[i].trim().startsWith("```")) { 28 | includeCodeBlock = true; 29 | break; 30 | } 31 | } 32 | if (includeCodeBlock) { 33 | return false; 34 | } 35 | } 36 | 37 | // 開始行から終了行までのすべての行が | で始まっていたらテーブル内 38 | for (let i = startline; i <= endline; i++) { 39 | if (!lines[i].trim().startsWith('|')) { 40 | return false; 41 | } 42 | } 43 | 44 | return true; 45 | } 46 | 47 | export function findTableRange(text: string, startline: number, endline: number): [number, number] | undefined { 48 | const lines = text.split(/\r\n|\n|\r/); 49 | 50 | // 指定範囲がテーブル内か確認する 51 | if (!isInTable(text, startline, endline)) { 52 | return undefined; 53 | } 54 | 55 | // 指定範囲の外にテーブルが広がっていたら範囲を修正 56 | let firstLine = startline; 57 | let lastLine = endline; 58 | while (firstLine - 1 >= 0) { 59 | const line_text = lines[firstLine - 1]; 60 | if (!line_text.trim().startsWith("|")) { 61 | break; 62 | } 63 | firstLine--; 64 | } 65 | while (lastLine + 1 < lines.length) { 66 | const line_text = lines[lastLine + 1]; 67 | if (!line_text.trim().startsWith("|")) { 68 | break; 69 | } 70 | lastLine++; 71 | } 72 | 73 | return [firstLine, lastLine]; 74 | } 75 | -------------------------------------------------------------------------------- /src/contextServices/textEditorContextServiceIsSupportedLanguage.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, Selection, TextDocument, window, StatusBarItem, StatusBarAlignment, TextEditor, TextEditorSelectionChangeEvent } from 'vscode'; 2 | import { ContextService } from "./contextService"; 3 | import { SupportLanguage } from '../definitions/supportLanguage'; 4 | 5 | export class TextEditorContextServiceIsSupportedLanguage extends ContextService { 6 | private statusBarItem: StatusBarItem | undefined; 7 | private readonly defaultVal: boolean; 8 | 9 | constructor(contextName: string, defaultVal: boolean) { 10 | super(contextName); 11 | this.defaultVal = defaultVal; 12 | } 13 | 14 | public onActivate(context: ExtensionContext) { 15 | super.onActivate(context); 16 | 17 | // set initial state of context 18 | this.setState(this.defaultVal); 19 | 20 | if(this.isDebugMode()) { 21 | // create a new status bar item that we can now manage 22 | this.statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 10000); 23 | context.subscriptions.push(this.statusBarItem); 24 | } 25 | } 26 | 27 | public onDidChangeActiveTextEditor(editor: TextEditor | undefined): void { 28 | super.onDidChangeActiveTextEditor(editor); 29 | this.updateContextState(editor); 30 | } 31 | 32 | public onDidChangeTextEditorSelection(event: TextEditorSelectionChangeEvent): void { 33 | super.onDidChangeTextEditorSelection(event); 34 | this.updateContextState(event.textEditor); 35 | } 36 | 37 | private isDebugMode(): boolean { 38 | return process.env.VSCODE_DEBUG_MODE === "true"; 39 | } 40 | 41 | private updateContextState(editor: TextEditor | undefined) { 42 | const document = editor?.document; 43 | var isSupported = SupportLanguage.isSupportedLanguage(document); 44 | 45 | if (isSupported) { 46 | this.setState(true); 47 | 48 | if (this.statusBarItem && document) { 49 | this.statusBarItem.text = `$(circle-large-filled) doc.languageId is ` + document.languageId; 50 | this.statusBarItem.tooltip = `supported language`; 51 | this.statusBarItem.show(); 52 | } 53 | } else { 54 | this.setState(false); 55 | 56 | if (this.statusBarItem && document) { 57 | this.statusBarItem.text = `$(circle-slash) doc.languageId is ` + document.languageId; 58 | this.statusBarItem.tooltip = `not supported language`; 59 | this.statusBarItem.show(); 60 | } 61 | else if (this.statusBarItem && !document) { 62 | this.statusBarItem.text = `$(circle-slash) document is undefined`; 63 | this.statusBarItem.tooltip = `not supported language`; 64 | this.statusBarItem.show(); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/contextServices/textEditorContextServiceCursorInTable.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, Selection, TextDocument, window, StatusBarItem, StatusBarAlignment, TextEditor, TextEditorSelectionChangeEvent } from 'vscode'; 2 | import { ContextService } from "./contextService"; 3 | import * as textUtility from "../textUtility"; 4 | 5 | export class TextEditorContextServiceCursorInTable extends ContextService { 6 | private statusBarItem: StatusBarItem | undefined = undefined; 7 | private readonly defaultVal: boolean; 8 | 9 | constructor(contextName: string, defaultVal: boolean) { 10 | super(contextName); 11 | this.defaultVal = defaultVal; 12 | } 13 | 14 | public onActivate(context: ExtensionContext) { 15 | super.onActivate(context); 16 | 17 | // set initial state of context 18 | this.setState(this.defaultVal); 19 | 20 | if(this.isDebugMode()) { 21 | // create a new status bar item that we can now manage 22 | this.statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 10000); 23 | context.subscriptions.push(this.statusBarItem); 24 | } 25 | } 26 | 27 | public onDidChangeActiveTextEditor(editor: TextEditor | undefined): void { 28 | super.onDidChangeActiveTextEditor(editor); 29 | this.updateContextState(editor); 30 | } 31 | 32 | public onDidChangeTextEditorSelection(event: TextEditorSelectionChangeEvent): void { 33 | super.onDidChangeTextEditorSelection(event); 34 | this.updateContextState(event.textEditor); 35 | } 36 | 37 | private isDebugMode(): boolean { 38 | return process.env.VSCODE_DEBUG_MODE === "true"; 39 | } 40 | 41 | private updateContextState(editor: TextEditor | undefined) { 42 | const document = editor?.document; 43 | const selection = editor?.selection; 44 | const isInTable = this.isInTable(document, selection); 45 | 46 | if (isInTable) { 47 | this.setState(true); 48 | 49 | if (this.statusBarItem) { 50 | this.statusBarItem.text = `$(circle-large-filled) in the table`; 51 | this.statusBarItem.tooltip = `cursor is in the table`; 52 | this.statusBarItem.show(); 53 | } 54 | } else { 55 | this.setState(false); 56 | 57 | if (this.statusBarItem) { 58 | this.statusBarItem.text = `$(circle-slash) out of table`; 59 | this.statusBarItem.tooltip = `cursor is out of table`; 60 | this.statusBarItem.show(); 61 | } 62 | } 63 | } 64 | 65 | private isInTable(document: TextDocument | undefined, selection: Selection | undefined) { 66 | if(!document || document.languageId === null) { 67 | return false; 68 | } 69 | 70 | if(document.languageId !== 'markdown' && document.languageId !== 'mdx' && document.languageId !== 'quarto') { 71 | return false; 72 | } 73 | 74 | if (!selection) { 75 | return false; 76 | } 77 | 78 | // 選択範囲がテーブル内か判定する 79 | const isInTable = textUtility.isInTable(document.getText(), selection.start.line, selection.end.line); 80 | return isInTable; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/test/sample/issue.md: -------------------------------------------------------------------------------- 1 | 2 | # No.22 3 | 4 | | C1 | C2 | C3 | 5 | | ----------- | --------- | --- | 6 | | 🧪 | Some text | | 7 | | Another row | More text | | 8 | 9 | # No.20 10 | 11 | | | | 12 | | ------------------------------- | --- | 13 | | Gyakorlatvezető | XYZ | 14 | | Gyakorlatvezető Gyakorlatvezető | XYZ | 15 | | Tanuló | XYZ | 16 | 17 | # No.3 18 | 19 | | EN | DE | 20 | | ------ | ------ | 21 | | Apple | Apfel | 22 | | Apples | Äpfel | 23 | | Banana | Banane | 24 | 25 | # No.57 26 | 27 | | column A | column B \| `|` | | column C | `\\` | column D | | 28 | | :------- | :-------------- | :----- | :------------- | :--- | ----------: | --- | 29 | | data A1 | data B1 | | data C1 | | \| data D1 | | 30 | | data A2 | data B2 `\\` | `\A\\` | \`\\`` data C2 | | `|`data D2 | | 31 | | data A3 | data B3 | | data C3 | | `|` data D3 | | 32 | | data A4 | data B4 | | data C4 | | `data| D4` | | 33 | 34 | 35 | | column A | column B | column C | column D | 36 | | :-- | :--: | --: | :-- | 37 | | data A1 | data B1 | data C1 | data D1 | 38 | | data A2 data A2 | data B2 | data C2 | data D2 | 39 | | data A3 | data B3 data B3 | data C3 | data D3 | 40 | | data A4 | data B4 | data C4 | data D4 | 41 | 42 | ```md 43 | | column A | column B \| `|` | | column C | `\\` | column D | | 44 | | :------- | :-------------- | :----- | :------------- | :--- | ----------: | --- | 45 | | data A1 | data B1 | | data C1 | | \| data D1 | | 46 | | data A2 | data B2 `\\` | `\A\\` | \`\\`` data C2 | | `|`data D2 | | 47 | | data A3 | data B3 | | data C3 | | `|` data D3 | | 48 | | data A4 | data B4 | | data C4 | | `data| D4` | | 49 | 50 | 51 | | column A | column B | column C | column D | 52 | | :-- | :--: | --: | :-- | 53 | | data A1 | data B1 | data C1 | data D1 | 54 | | data A2 data A2 | data B2 | data C2 | data D2 | 55 | | data A3 | data B3 data B3 | data C3 | data D3 | 56 | | data A4 | data B4 | data C4 | data D4 | 57 | ``` 58 | 59 | | column A | column B \| `|` | | column C | `\\` | column D | | 60 | | :------- | :-------------- | :----- | :------------- | :--- | ----------: | --- | 61 | | data A1 | data B1 | | data C1 | | \| data D1 | | 62 | | data A2 | data B2 `\\` | `\A\\` | \`\\`` data C2 | | `|`data D2 | | 63 | | data A3 | data B3 | | data C3 | | `|` data D3 | | 64 | | data A4 | data B4 | | data C4 | | `data| D4` | | 65 | 66 | 67 | | column A | column B | column C | column D | 68 | | :-- | :--: | --: | :-- | 69 | | data A1 | data B1 | data C1 | data D1 | 70 | | data A2 data A2 | data B2 | data C2 | data D2 | 71 | | data A3 | data B3 data B3 | data C3 | data D3 | 72 | | data A4 | data B4 | data C4 | data D4 | 73 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as vscode from 'vscode'; 4 | import * as commands from './commands'; 5 | import { ContextServiceManager } from "./contextServices/contextServiceManager"; 6 | import { FormatOnSaveService } from './doOnSaveServices/formatOnSaveService'; 7 | 8 | 9 | // this method is called when your extension is activated 10 | // your extension is activated the very first time the command is executed 11 | export function activate(context: vscode.ExtensionContext) { 12 | 13 | // Use the console to output diagnostic information (console.log) and errors (console.error) 14 | // This line of code will only be executed once when your extension is activated 15 | //console.log('Congratulations, your extension "markdowntable" is now active!'); 16 | 17 | // The command has been defined in the package.json file 18 | // Now provide the implementation of the command with registerCommand 19 | // The commandId parameter must match the command field in package.json 20 | 21 | // custom context key services 22 | const contextServiceManager = new ContextServiceManager(); 23 | // subscribe custom context key services 24 | context.subscriptions.push( 25 | contextServiceManager 26 | ); 27 | // start custom context key services 28 | contextServiceManager.activate(context); 29 | 30 | // subscribe and activate formatOnSaveService 31 | const formatOnSaveService = new FormatOnSaveService(); 32 | context.subscriptions.push( 33 | formatOnSaveService 34 | ); 35 | formatOnSaveService.activate(context); 36 | 37 | // subscribe command handlers 38 | context.subscriptions.push( 39 | vscode.commands.registerCommand('markdowntable.nextCell', () => commands.navigateNextCell(true)), 40 | vscode.commands.registerCommand('markdowntable.prevCell', () => commands.navigatePrevCell(true)), 41 | vscode.commands.registerCommand('markdowntable.nextCellWithoutFormat', () => commands.navigateNextCell(false)), 42 | vscode.commands.registerCommand('markdowntable.prevCellWithoutFormat', () => commands.navigatePrevCell(false)), 43 | vscode.commands.registerCommand('markdowntable.tsvToTable', () => commands.tsvToTable()), 44 | vscode.commands.registerCommand('markdowntable.csvToTable', () => commands.csvToTable()), 45 | vscode.commands.registerCommand('markdowntable.format', () => commands.formatAll()), 46 | vscode.commands.registerCommand('markdowntable.insertRight', () => commands.insertColumn(false)), 47 | vscode.commands.registerCommand('markdowntable.insertLeft', () => commands.insertColumn(true)), 48 | vscode.commands.registerCommand('markdowntable.alignLeft', () => commands.alignColumns([':', '-'])), 49 | vscode.commands.registerCommand('markdowntable.alignCenter', () => commands.alignColumns([':', ':'])), 50 | vscode.commands.registerCommand('markdowntable.alignRight', () => commands.alignColumns(['-', ':'])), 51 | vscode.commands.registerCommand('markdowntable.moveLeft', () => commands.moveColumns(true)), 52 | vscode.commands.registerCommand('markdowntable.moveRight', () => commands.moveColumns(false)) 53 | ); 54 | } 55 | 56 | 57 | // this method is called when your extension is deactivated 58 | export function deactivate() { } 59 | -------------------------------------------------------------------------------- /src/contextServices/contextServiceManager.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, window, Disposable, ConfigurationChangeEvent, TextEditor, TextEditorSelectionChangeEvent, workspace } from 'vscode'; 2 | import { ContextService } from "./contextService"; 3 | import { TextEditorContextServiceCursorInTable } from "./textEditorContextServiceCursorInTable"; 4 | import { TextEditorContextServiceIsSupportedLanguage } from "./textEditorContextServiceIsSupportedLanguage"; 5 | import { WorkspaceBooleanConfigurationContextService } from "./workspaceBooleanConfigurationContextService"; 6 | 7 | export class ContextServiceManager implements Disposable { 8 | private readonly contextServices: Array = []; 9 | 10 | public constructor() { 11 | // push context services 12 | this.contextServices.push( 13 | new TextEditorContextServiceCursorInTable("markdowntable.contextkey.selection.InMarkdownTable", false), 14 | new TextEditorContextServiceIsSupportedLanguage("markdowntable.contextkey.active.IsSupportedLanguage", false), 15 | new WorkspaceBooleanConfigurationContextService("markdowntable.contextkey.config.showMenu.format", 'markdowntable.showMenu.format'), 16 | new WorkspaceBooleanConfigurationContextService("markdowntable.contextkey.config.showMenu.tsvToTable", 'markdowntable.showMenu.tsvToTable'), 17 | new WorkspaceBooleanConfigurationContextService("markdowntable.contextkey.config.showMenu.csvToTable", 'markdowntable.showMenu.csvToTable'), 18 | new WorkspaceBooleanConfigurationContextService("markdowntable.contextkey.config.showMenu.insertRight", 'markdowntable.showMenu.insertRight'), 19 | new WorkspaceBooleanConfigurationContextService("markdowntable.contextkey.config.showMenu.insertLeft", 'markdowntable.showMenu.insertLeft'), 20 | new WorkspaceBooleanConfigurationContextService("markdowntable.contextkey.config.showMenu.alignLeft", 'markdowntable.showMenu.alignLeft'), 21 | new WorkspaceBooleanConfigurationContextService("markdowntable.contextkey.config.showMenu.alignCenter", 'markdowntable.showMenu.alignCenter'), 22 | new WorkspaceBooleanConfigurationContextService("markdowntable.contextkey.config.showMenu.alignRight", 'markdowntable.showMenu.alignRight'), 23 | new WorkspaceBooleanConfigurationContextService("markdowntable.contextkey.config.showMenu.moveLeft", 'markdowntable.showMenu.moveLeft'), 24 | new WorkspaceBooleanConfigurationContextService("markdowntable.contextkey.config.showMenu.moveRight", 'markdowntable.showMenu.moveRight'), 25 | ); 26 | } 27 | 28 | public activate(context: ExtensionContext) { 29 | for (const service of this.contextServices) { 30 | service.onActivate(context); 31 | } 32 | // subscribe update handler for context 33 | context.subscriptions.push( 34 | window.onDidChangeActiveTextEditor((editor) => this.onDidChangeActiveTextEditor(editor)), 35 | window.onDidChangeTextEditorSelection((event) => this.onDidChangeTextEditorSelection(event)), 36 | workspace.onDidChangeConfiguration((event) => this.onDidChangeConfiguration(event)) 37 | ); 38 | } 39 | 40 | public dispose(): void { 41 | while (this.contextServices.length > 0) { 42 | const service = this.contextServices.pop(); 43 | service!.dispose(); 44 | } 45 | } 46 | 47 | private onDidChangeActiveTextEditor(editor: TextEditor | undefined): void { 48 | for (const service of this.contextServices) { 49 | service.onDidChangeActiveTextEditor(editor); 50 | } 51 | } 52 | 53 | private onDidChangeTextEditorSelection(event: TextEditorSelectionChangeEvent): void { 54 | for (const service of this.contextServices) { 55 | service.onDidChangeTextEditorSelection(event); 56 | } 57 | } 58 | 59 | private onDidChangeConfiguration(event: ConfigurationChangeEvent): void { 60 | for (const service of this.contextServices) { 61 | service.onDidChangeConfiguration(event); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/markdownTableUtility.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | /** 4 | * 1行分の文字列を列データ配列に分解する 5 | * 指定した列数に満たない行は 埋め文字 で埋める 6 | * @param linestr 1行分の文字列 7 | * @param columnNum 列数 8 | * @param fillstr 埋め文字 9 | */ 10 | export function splitline(linestr: string, columnNum: number, fillstr: string = '') { 11 | // 先頭と末尾の|を削除 12 | linestr = linestr.trim(); 13 | if (linestr.startsWith('|')) { 14 | linestr = linestr.slice(1); 15 | } 16 | if (linestr.endsWith('|')) { 17 | linestr = linestr.slice(0, -1); 18 | } 19 | 20 | // |で分割 21 | let linedatas: string[] = []; 22 | let startindex = 0; 23 | let endindex = 0; 24 | let isEscaping = false; 25 | let isInInlineCode = false; 26 | for (let i = 0; i < linestr.length; ++i) { 27 | if (isEscaping) { 28 | // エスケープ文字の次の文字は|かどうか判定しない 29 | isEscaping = false; 30 | endindex++; 31 | continue; 32 | } 33 | 34 | const chara = linestr.charAt(i); 35 | if (chara === '\`') { 36 | // `の間はインラインコード 37 | isInInlineCode = !isInInlineCode; 38 | endindex++; 39 | continue; 40 | } 41 | if (isInInlineCode) { 42 | // インラインコード中は|かどうか判定しない 43 | endindex++; 44 | continue; 45 | } 46 | 47 | if (chara === '\\') { 48 | // \はエスケープ文字 49 | isEscaping = true; 50 | endindex++; 51 | continue; 52 | } 53 | 54 | if (chara !== '|') { 55 | // | 以外だったら継続 56 | endindex++; 57 | continue; 58 | } 59 | 60 | // | だったら分割 61 | let cellstr = linestr.slice(startindex, endindex); 62 | linedatas.push(cellstr); 63 | startindex = i + 1; 64 | endindex = i + 1; 65 | } 66 | linedatas.push(linestr.slice(startindex)); 67 | 68 | // データ数分を''で埋めておく 69 | let datas: string[] = new Array(columnNum).fill(fillstr); 70 | // 行文字列から取得したデータに置き換える 71 | for (let i = 0; i < linedatas.length; i++) { 72 | datas[i] = linedatas[i]; 73 | } 74 | return datas; 75 | }; 76 | 77 | 78 | 79 | // 半角文字は1文字、全角文字は2文字として文字数をカウントする 80 | export function getLen(str: string): number { 81 | let length = 0; 82 | for (let i = 0; i < str.length; i++) { 83 | let chp = str.codePointAt(i); 84 | if (chp === undefined) { 85 | continue; 86 | } 87 | let chr = chp as number; 88 | if (doesUse0Space(chr)) { 89 | length += 0; 90 | } 91 | else if (doesUse3Spaces(chr)) { 92 | length += 3; 93 | } 94 | else if (doesUse2Spaces(chr)) { 95 | // 全角文字の場合は2を加算 96 | length += 2; 97 | } 98 | else { 99 | //それ以外の文字の場合は1を加算 100 | length += 1; 101 | } 102 | 103 | let chc = str.charCodeAt(i); 104 | if (chc >= 0xD800 && chc <= 0xDBFF) { 105 | // サロゲートペアの時は1文字読み飛ばす 106 | i++; 107 | } 108 | 109 | // if( (chr >= 0x00 && chr <= 0x80) || 110 | // (chr >= 0xa0 && chr <= 0xff) || 111 | // (chr === 0xf8f0) || 112 | // (chr >= 0xff61 && chr <= 0xff9f) || 113 | // (chr >= 0xf8f1 && chr <= 0xf8f3)){ 114 | // //半角文字の場合は1を加算 115 | // length += 1; 116 | // }else{ 117 | // //それ以外の文字の場合は2を加算 118 | // length += 2; 119 | // } 120 | } 121 | //結果を返す 122 | return length; 123 | }; 124 | 125 | function doesUse0Space(charCode: number): boolean { 126 | if ((charCode === 0x02DE) || 127 | (charCode >= 0x0300 && charCode <= 0x036F) || 128 | (charCode >= 0x0483 && charCode <= 0x0487) || 129 | (charCode >= 0x0590 && charCode <= 0x05CF)) { 130 | return true; 131 | } 132 | return false; 133 | } 134 | 135 | function doesUse2Spaces(charCode: number): boolean { 136 | if ((charCode >= 0x2480 && charCode <= 0x24FF) || 137 | (charCode >= 0x2600 && charCode <= 0x27FF) || 138 | (charCode >= 0x2900 && charCode <= 0x2CFF) || 139 | (charCode >= 0x2E00 && charCode <= 0xFF60) || 140 | (charCode >= 0xFFA0)) { 141 | return true; 142 | } 143 | return false; 144 | } 145 | 146 | function doesUse3Spaces(charCode: number): boolean { 147 | if (charCode >= 0x1F300 && charCode <= 0x1FBFF) { 148 | return true; 149 | } 150 | return false; 151 | } 152 | 153 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## All notable changes to the "markdowntable" extension will be documented in this file 4 | 5 | ### 0.13.0 6 | 7 | - [Add] Confiuration markdowntable.paddedDelimiterRowPipes 8 | 9 | ### 0.12.0 10 | 11 | - [Add] Configuration to format on save 12 | 13 | ### 0.11.0 14 | 15 | - [Change] VSCode engine to 1.80.0 16 | - [Change] Ignore tables in codeblock 17 | - [Add] Configuration to switch ignore or not ignore tables in codeblock 18 | - [Add] Configurations to hide menu items in right click menu 19 | - [Add] Convert CSV to table command 20 | - [Add] Commands to move columns to right/left 21 | 22 | ### 0.10.4 and 0.10.3-PreRelease (2023/3/15) 23 | 24 | - [Add] Extention Icon 25 | - [Add] Quarto support (.qmd) 26 | - [Change] Vscode engine up to 1.63.0 27 | 28 | ### 0.10.2 (2022/9/5) 29 | 30 | - [Change] Reduce requirement for insert commands 31 | - Allow selecting range 32 | 33 | ### 0.10.1 (2022/8/10) 34 | 35 | - [Change] Remove key binding `Shift+Alt+F` (for markdowntable.format) 36 | - [Change] Remove key binding `Shift+Alt+T` (for markdowntable.tsvToTable) 37 | 38 | ### 0.10.0 (2022/7/9) 39 | 40 | - [Change] Select all text in cell when "Tab" or "Shift+Tab" navigation (fix #48) 41 | 42 | ### 0.9.2 (2022/4/13) 43 | 44 | - [Fix] Add notice in readme (fix #43) 45 | - [Fix] Update packages 46 | - [Fix] Add null check (fix #44) 47 | 48 | ### 0.9.1 (2022/3/14) 49 | 50 | - [Fix] Bug: Status message for debug is found out in status bar 51 | 52 | ### 0.9.0 (2022/3/11) 53 | 54 | - [Update] Added contributor section in readme 55 | - [Fix] Shorten the way to get text in cursor line 56 | - [Fix] Added context key to context menu 57 | - Hidding context menu when the cursor is not in table region 58 | 59 | ### 0.8.0 (2022/1/25) 60 | 61 | - [Fix] Rearranged the order of escape judgment and inline code judgment (fix #36) 62 | - [Fix] Add custom context key for nextCell/prevCell command's key binding 63 | - To improve "when" assign of commands to be more suitable. 64 | - To resolve Tab key confliction with other extensions. 65 | - [Fix] When nextCell/prevCell is called in 2nd row of table, cursol doesn't go next/prev cell correctoly. 66 | 67 | ### 0.7.2 (2021/12/06) 68 | 69 | - [Fix] Navigation slippage while a escaped or inline-coded pipe character is in table. 70 | 71 | ### 0.7.1 (2021/12/05) 72 | 73 | - [Fix] Allow inline code `\` and escaped \\\| in table. 74 | - [Change] Shorten config title. "Markdown Table Configuration" -> "Markdown Table" 75 | - [Fix] Improve processing, don't get the full text. 76 | - [Fix] Update npm packages. 77 | 78 | ### 0.7.0 (2021/09/11) 79 | 80 | - [Add] Add tips for snippet suggestion to generate simple table 81 | 82 | ### 0.6.0 (2021/06/15) 83 | 84 | - [Add] Support mdx 85 | - [Fix] Activate earlier (Activate extension on markdown file is openning) 86 | 87 | ### 0.5.2 (2021/05/04) 88 | 89 | - [Fix] zenkaku handling 90 | 91 | ### 0.5.1 (2021/03/19) 92 | 93 | - [Fix] Readme only 94 | - Default value of configuration 95 | 96 | ### 0.5.0 (2021/03/19) 97 | 98 | - [Add] 99 | - Align data and column header when formatting table 100 | - It can be disabled by configuration 101 | 102 | ### 0.4.2 (2021/01/13) 103 | 104 | - [Fix] 105 | - Change supported vscode version to "upper than 1.40.0" from "upper than 1.50.0" 106 | 107 | ### 0.4.1 (2020/11/05) 108 | 109 | - Readme update only 110 | 111 | ### 0.4.0 (2020/11/05) 112 | 113 | - [Add] Navigate next/prev cell without auto format command. 114 | - [Fix] Typo: "Insert Row" command should be "Insert Column". 115 | 116 | ### 0.3.0 (2020/10/27) 117 | 118 | - [Add] Align column/columns commands 119 | - Align column to left/center/right. 120 | - ![align](images/align_column.gif) 121 | - Align selected multi columns at once. 122 | - ![align](images/align_columns_at_once.gif) 123 | 124 | ### 0.2.2 (2020/9/5) 125 | 126 | - [Fix] 127 | - Tab key confliction with accepting suggestion or snippet. 128 | 129 | ### 0.2.1 (2020/9/2) 130 | 131 | - [Fix] bugs of navigate to next/prev cell 132 | - Bahavior when Tab key pressing out of table. 133 | - Bahavior when Shift+Tab key pressing out of table. 134 | 135 | ### 0.2.0 (2020/9/1) 136 | 137 | - [Fix] Keep white spaces indentation on the left of table when formatting. 138 | - before 139 | - ![keepindent_before](images/keep_indent_before.gif) 140 | - after 141 | - ![keepindent](images/keep_indent.gif) 142 | 143 | - [Add] Navigate to prev cell when you press Shift+Tab key in table. 144 | - ![navigate_prev](images/navigate_prev_cell.gif) 145 | 146 | ### 0.1.1 (2020/08/07) 147 | 148 | - [Fix] Treat "umlauts" as half-width character. 149 | 150 | ### 0.1.0 (2020/08/01) 151 | 152 | - [Add] Navigate to next cell when you press Tab key in table. 153 | 154 | ### 0.0.2 (2020/07/23) 155 | 156 | - Initial release 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markdown Table 2 | 3 | Add features to edit markdown table. 4 | 5 | ## 1. Features 6 | 7 | | Title | Command | Keybinding | In the Editor Right Click Menu | 8 | | :-------------------------------------- | :---------------------------------- | :---------- | :----------------------------------- | 9 | | Navigate to next cell. | markdowntable.nextCell | Tab | No | 10 | | Navigate to previous cell. | markdowntable.prevCell | Shift + Tab | No | 11 | | Navigate to next cell (w/o format). | markdowntable.nextCellWithoutFormat | | No | 12 | | Navigate to previous cell (w/o format). | markdowntable.prevCellWithoutFormat | | No | 13 | | Format all tables. | markdowntable.format | | Yes (*1) | 14 | | Convert TSV to table. | markdowntable.tsvToTable | | Yes (only when selecting range) (*1) | 15 | | Convert CSV to table. | markdowntable.csvToTable | | No (only when selecting range) (*1) | 16 | | Insert column in the right. | markdowntable.insertRight | | Yes (*1) | 17 | | Insert column in the left. | markdowntable.insertLeft | | Yes (*1) | 18 | | Align to Left. | markdowntable.alignLeft | | Yes (*1) | 19 | | Align to Center. | markdowntable.alignCenter | | Yes (*1) | 20 | | Align to Right. | markdowntable.alignRight | | Yes (*1) | 21 | | Move to Left. | markdowntable.moveLeft | | Yes (*1) | 22 | | Move to Right. | markdowntable.moveRight | | Yes (*1) | 23 | 24 | (*1) By using configuration, you can switch show or hide menu item. 25 | 26 | --- 27 | 28 | ## 2. Demo 29 | 30 | ### 2.1. Navigate to next cell (with auto format & auto insert row) 31 | 32 | Key binding to `Tab`. 33 | 34 | - **Auto navigate to next cell when pressing Tab key in table.** 35 | - When out of table, vscode's default "tab" behavior is operated. 36 | - **Auto insert new row, when the cursor is in last row in table.** 37 | - with auto format 38 | - ![navigate](images/navigate_next_cell.gif) 39 | 40 | #### 2.1.1. If you want to use it without auto format 41 | 42 | - Use markdowntable.nextCellWithoutFormat command 43 | - If you want, you need to assign this command to some key binding by yourself. 44 | 45 | ### 2.2. Navigate to prev cell 46 | 47 | Key binding to `Shift`+`Tab`. 48 | 49 | - **Auto navigate to prev cell when pressing Shift+Tab key in table.** 50 | - When out of table, vscode's default "outdent" behavior is operated. 51 | - with auto format 52 | - ![navigate_prev](images/navigate_prev_cell.gif) 53 | 54 | #### 2.2.1. If you want to use it without auto format 55 | 56 | - Use markdowntable.prevCellWithoutFormat command 57 | - If you want, you need to assign this command to some key binding by yourself. 58 | 59 | ### 2.3. Convert from text to table 60 | 61 | - Convert from TSV 62 | - **Tips: This feature is supposed to make table from excel cells.** 63 | - ![convert](images/table_from_excel.gif) 64 | - Convert from CSV 65 | - **Note: This menu item is hidden in default. You can show it by setting markdowntable.showMenu.csvToTable configuration as true.** 66 | - ![convert](images/csv_to_table.gif) 67 | 68 | ### 2.4. Insert column 69 | 70 | - Add context menu to insert column 71 | - ![insert](images/insert.gif) 72 | 73 | ### 2.6. Move column/columns 74 | 75 | - **Move selected multi columns at once.** 76 | - ![align](images/move_columns_at_once.gif) 77 | 78 | ### 2.5. Align column/columns 79 | 80 | - **Align column to left/center/right.** 81 | - ![align](images/align_column.gif) 82 | 83 | - **Align selected multi columns at once.** 84 | - ![align](images/align_columns_at_once.gif) 85 | 86 | ### 2.7. Format table 87 | 88 | - **Auto format column width of all tables in current document** 89 | - Align data and column header (can be disabled by configuration) 90 | - ![formattable](images/format_table.gif) 91 | 92 | ## 3. Extension Configurations 93 | 94 | | Configuration ID | Description | Type | Default | 95 | | :------------------------------------ | :--------------------------------------------------------- | :------ | :------ | 96 | | markdowntable.alignColumnHeader | Align column header in the table when formatting | boolean | true | 97 | | markdowntable.alignData | Align data in the table when formatting | boolean | true | 98 | | markdowntable.ignoreCodeblock | Ignore tables in code block | boolean | true | 99 | | markdowntable.paddedDelimiterRowPipes | Add spaces around delimiter row pipes | boolean | true | 100 | | markdowntable.showMenu.format | Show command in context menu, "Format all tables" | boolean | true | 101 | | markdowntable.showMenu.tsvToTable | Show command in context menu, "Convert TSV to table" | boolean | true | 102 | | markdowntable.showMenu.csvToTable | Show command in context menu, "Convert CSV to table" | boolean | false | 103 | | markdowntable.showMenu.insertRight | Show command in context menu, "Insert column to the right" | boolean | true | 104 | | markdowntable.showMenu.insertLeft | Show command in context menu, "Insert column to the left" | boolean | true | 105 | | markdowntable.showMenu.alignLeft | Show command in context menu, "Align to Left" | boolean | true | 106 | | markdowntable.showMenu.alignCenter | Show command in context menu, "Align to Center" | boolean | true | 107 | | markdowntable.showMenu.alignRight | Show command in context menu, "Align to Right" | boolean | true | 108 | | markdowntable.showMenu.moveLeft | Show command in context menu, "Move to Left" | boolean | true | 109 | | markdowntable.showMenu.moveRight | Show command in context menu, "Move to Right" | boolean | true | 110 | | markdowntable.formatOnSave | Format all tables on save | boolean | false | 111 | 112 | ## 4. Tips 113 | 114 | ### 4.1. Add a snippet to create a simple table 115 | 116 | You can define user snippets. 117 | 118 | - References 119 | - [Snippets in Visual Studio Code (Official document)](https://code.visualstudio.com/docs/editor/userdefinedsnippets) 120 | - Steps to add a snippet **table** to the global scope 121 | 1. Open snippet file 122 | 1. Select User Snippets under File > Preferences (Code > Preferences on macOS) 123 | 1. Select markdown.json or markdown under New Global Snippets file 124 | 1. Add the following, and save file 125 | 126 | ```json 127 | { 128 | "Insert a simple table": { 129 | "prefix": "table", 130 | "body": [ 131 | "|${0:title} | |", 132 | "| - | - |", 133 | "| | |" 134 | ], 135 | "description": "Insert a simple table" 136 | } 137 | } 138 | ``` 139 | 140 | - Step to add a snippet **table** to the project scope 141 | 1. Create ".vscode/markdown.code-snippets" in your project directory 142 | 1. Add the following, and save file 143 | - Syntax is almost the same as global, scope option is additional 144 | 145 | ```json 146 | { 147 | "Insert a simple table": { 148 | "prefix": "table", 149 | "scope": "markdown", 150 | "body": [ 151 | "|${0:title} | |", 152 | "| - | - |", 153 | "| | |" 154 | ], 155 | "description": "Insert a simple table" 156 | } 157 | } 158 | ``` 159 | 160 | ### 4.2. Enable snippets suggestion 161 | 162 | By default, the snippets suggestion is disabled in markdown. 163 | You need to enable it to use. 164 | 165 | - References 166 | - [User and Workspace Settings (Official document)](https://code.visualstudio.com/docs/getstarted/settings) 167 | - Step to enable snippets suggestion to the global scope 168 | 1. Open user settings file 169 | - Windows %APPDATA%\Code\User\settings.json 170 | - macOS $HOME/Library/Application Support/Code/User/settings.json 171 | - Linux $HOME/.config/Code/User/settings.json 172 | 1. Add the following, and save file 173 | 174 | ```json 175 | "[markdown]": { 176 | "editor.quickSuggestions": true 177 | } 178 | ``` 179 | 180 | - Step to enable snippets suggestion to the project stope 181 | 1. Create (or open if already exist) ".vscode/settings.json" in your project directory 182 | 1. Add the following, and save file 183 | 184 | ```json 185 | "[markdown]": { 186 | "editor.quickSuggestions": true 187 | } 188 | ``` 189 | 190 | ## 5. Notice 191 | 192 | - Tables have to be consisted by using GFM spec 193 | - a header row, a delimiter row, and zero or more data rows 194 | - leading pipe is needed in each rows 195 | - NOT support following style 196 | 197 | ```markdown 198 | abc | defghi 199 | --- | :-------- 200 | bar | baz 201 | ``` 202 | 203 | ## 6. Policy 204 | 205 | What's focused on. 206 | 207 | - As minimal 208 | - Not enhance or change markdown syntax spec. 209 | - Not implement features they can be done by vscode's box selection. 210 | - [https://code.visualstudio.com/docs/editor/codebasics#_column-box-selection](https://code.visualstudio.com/docs/editor/codebasics#_column-box-selection) 211 | - Support full-width characters 212 | - Because author is Japanese 213 | 214 | ## 7. Thanks 215 | 216 | Special thanks to contributors. 217 | 218 | - M.A 219 | - [Mark Ferrall](https://github.com/mferrall) 220 | - [heartacker](https://github.com/heartacker) 221 | - [jimtng](https://github.com/jimtng) 222 | 223 | ## 8. Release Notes 224 | 225 | - See [changelog](CHANGELOG.md). 226 | 227 | ## 9. Links 228 | 229 | - [Source Code](https://github.com/takumisoft68/vscode-markdown-table) 230 | - [Marketplace](https://marketplace.visualstudio.com/items?itemName=TakumiI.markdowntable) 231 | - [VSX Registry](https://open-vsx.org/extension/TakumiI/markdowntable) 232 | 233 | ## 10. License 234 | 235 | Apache 2.0, See [LICENSE](LICENSE) for more information. 236 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | 179 | Copyright 2021 takumisoft68 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdowntable", 3 | "publisher": "TakumiI", 4 | "displayName": "Markdown Table", 5 | "description": "Add features to edit markdown table.", 6 | "version": "0.13.0", 7 | "icon": "icon.drawio.png", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/takumisoft68/vscode-markdown-table" 11 | }, 12 | "engines": { 13 | "vscode": "^1.80.0" 14 | }, 15 | "categories": [ 16 | "Other" 17 | ], 18 | "activationEvents": [ 19 | "onLanguage:markdown", 20 | "onLanguage:mdx", 21 | "onLanguage:quarto" 22 | ], 23 | "main": "./out/extension.js", 24 | "contributes": { 25 | "commands": [ 26 | { 27 | "command": "markdowntable.format", 28 | "title": "Markdown Table: Format all tables." 29 | }, 30 | { 31 | "command": "markdowntable.tsvToTable", 32 | "title": "Markdown Table: Convert TSV to table." 33 | }, 34 | { 35 | "command": "markdowntable.csvToTable", 36 | "title": "Markdown Table: Convert CSV to table." 37 | }, 38 | { 39 | "command": "markdowntable.insertRight", 40 | "title": "Markdown Table: Insert column to the right." 41 | }, 42 | { 43 | "command": "markdowntable.insertLeft", 44 | "title": "Markdown Table: Insert column to the left." 45 | }, 46 | { 47 | "command": "markdowntable.nextCell", 48 | "title": "Markdown Table: Navigate to next cell." 49 | }, 50 | { 51 | "command": "markdowntable.prevCell", 52 | "title": "Markdown Table: Navigate to previous cell." 53 | }, 54 | { 55 | "command": "markdowntable.nextCellWithoutFormat", 56 | "title": "Markdown Table: Navigate to next cell (without auto format)." 57 | }, 58 | { 59 | "command": "markdowntable.prevCellWithoutFormat", 60 | "title": "Markdown Table: Navigate to previous cell (without auto format)." 61 | }, 62 | { 63 | "command": "markdowntable.alignLeft", 64 | "title": "Markdown Table: Align to Left." 65 | }, 66 | { 67 | "command": "markdowntable.alignCenter", 68 | "title": "Markdown Table: Align to Center." 69 | }, 70 | { 71 | "command": "markdowntable.alignRight", 72 | "title": "Markdown Table: Align to Right." 73 | }, 74 | { 75 | "command": "markdowntable.moveLeft", 76 | "title": "Markdown Table: Move to Left." 77 | }, 78 | { 79 | "command": "markdowntable.moveRight", 80 | "title": "Markdown Table: Move to Right." 81 | } 82 | ], 83 | "keybindings": [ 84 | { 85 | "command": "markdowntable.nextCell", 86 | "key": "tab", 87 | "when": "editorTextFocus && !editorReadonly && !editorTabMovesFocus && !suggestWidgetMultipleSuggestions && !suggestWidgetVisible && !inSnippetMode && markdowntable.contextkey.selection.InMarkdownTable" 88 | }, 89 | { 90 | "command": "markdowntable.prevCell", 91 | "key": "shift+tab", 92 | "when": "editorTextFocus && !editorReadonly && !editorTabMovesFocus && !suggestWidgetMultipleSuggestions && !suggestWidgetVisible && !inSnippetMode && markdowntable.contextkey.selection.InMarkdownTable" 93 | } 94 | ], 95 | "menus": { 96 | "editor/context": [ 97 | { 98 | "when": "markdowntable.contextkey.active.IsSupportedLanguage && markdowntable.contextkey.config.showMenu.format", 99 | "command": "markdowntable.format", 100 | "group": "markdowntable@1" 101 | }, 102 | { 103 | "when": "markdowntable.contextkey.active.IsSupportedLanguage && editorHasSelection && markdowntable.contextkey.config.showMenu.tsvToTable", 104 | "command": "markdowntable.tsvToTable", 105 | "group": "markdowntable@2" 106 | }, 107 | { 108 | "when": "markdowntable.contextkey.active.IsSupportedLanguage && editorHasSelection && markdowntable.contextkey.config.showMenu.csvToTable", 109 | "command": "markdowntable.csvToTable", 110 | "group": "markdowntable@2" 111 | }, 112 | { 113 | "when": "markdowntable.contextkey.active.IsSupportedLanguage && markdowntable.contextkey.selection.InMarkdownTable && markdowntable.contextkey.config.showMenu.insertRight", 114 | "command": "markdowntable.insertRight", 115 | "group": "markdowntable@3" 116 | }, 117 | { 118 | "when": "markdowntable.contextkey.active.IsSupportedLanguage && markdowntable.contextkey.selection.InMarkdownTable && markdowntable.contextkey.config.showMenu.insertLeft", 119 | "command": "markdowntable.insertLeft", 120 | "group": "markdowntable@4" 121 | }, 122 | { 123 | "when": "markdowntable.contextkey.active.IsSupportedLanguage && markdowntable.contextkey.selection.InMarkdownTable && markdowntable.contextkey.config.showMenu.alignLeft", 124 | "command": "markdowntable.alignLeft", 125 | "group": "markdowntable@5" 126 | }, 127 | { 128 | "when": "markdowntable.contextkey.active.IsSupportedLanguage && markdowntable.contextkey.selection.InMarkdownTable && markdowntable.contextkey.config.showMenu.alignCenter", 129 | "command": "markdowntable.alignCenter", 130 | "group": "markdowntable@6" 131 | }, 132 | { 133 | "when": "markdowntable.contextkey.active.IsSupportedLanguage && markdowntable.contextkey.selection.InMarkdownTable && markdowntable.contextkey.config.showMenu.alignRight", 134 | "command": "markdowntable.alignRight", 135 | "group": "markdowntable@7" 136 | }, 137 | { 138 | "when": "markdowntable.contextkey.active.IsSupportedLanguage && markdowntable.contextkey.selection.InMarkdownTable && markdowntable.contextkey.config.showMenu.moveLeft", 139 | "command": "markdowntable.moveLeft", 140 | "group": "markdowntable@8" 141 | }, 142 | { 143 | "when": "markdowntable.contextkey.active.IsSupportedLanguage && markdowntable.contextkey.selection.InMarkdownTable && markdowntable.contextkey.config.showMenu.moveRight", 144 | "command": "markdowntable.moveRight", 145 | "group": "markdowntable@8" 146 | } 147 | ] 148 | }, 149 | "configuration": { 150 | "title": "Markdown Table", 151 | "properties": { 152 | "markdowntable.alignData": { 153 | "type": "boolean", 154 | "default": true, 155 | "description": "Align data in the table when formatting" 156 | }, 157 | "markdowntable.alignColumnHeader": { 158 | "type": "boolean", 159 | "default": true, 160 | "description": "Align column header in the table when formatting" 161 | }, 162 | "markdowntable.ignoreCodeblock": { 163 | "type": "boolean", 164 | "default": true, 165 | "description": "Ignore tables in codeblock" 166 | }, 167 | "markdowntable.paddedDelimiterRowPipes": { 168 | "type": "boolean", 169 | "default": true, 170 | "description": "Add spaces around delimiter row pipes." 171 | }, 172 | "markdowntable.showMenu.format": { 173 | "type": "boolean", 174 | "default": true, 175 | "description": "Show command in context menu: \"Format all tables\"." 176 | }, 177 | "markdowntable.showMenu.tsvToTable": { 178 | "type": "boolean", 179 | "default": true, 180 | "description": "Show command in context menu: \"Convert TSV to table\"." 181 | }, 182 | "markdowntable.showMenu.csvToTable": { 183 | "type": "boolean", 184 | "default": false, 185 | "description": "Show command in context menu: \"Convert CSV to table\"." 186 | }, 187 | "markdowntable.showMenu.insertRight": { 188 | "type": "boolean", 189 | "default": true, 190 | "description": "Show command in context menu: \"Insert column to the right\"." 191 | }, 192 | "markdowntable.showMenu.insertLeft": { 193 | "type": "boolean", 194 | "default": true, 195 | "description": "Show command in context menu: \"Insert column to the left\"." 196 | }, 197 | "markdowntable.showMenu.alignLeft": { 198 | "type": "boolean", 199 | "default": true, 200 | "description": "Show command in context menu: \"Align to Left\"." 201 | }, 202 | "markdowntable.showMenu.alignCenter": { 203 | "type": "boolean", 204 | "default": true, 205 | "description": "Show command in context menu: \"Align to Center\"." 206 | }, 207 | "markdowntable.showMenu.alignRight": { 208 | "type": "boolean", 209 | "default": true, 210 | "description": "Show command in context menu: \"Align to Right\"." 211 | }, 212 | "markdowntable.showMenu.moveLeft": { 213 | "type": "boolean", 214 | "default": true, 215 | "description": "Show command in context menu: \"Move to Left\"." 216 | }, 217 | "markdowntable.showMenu.moveRight": { 218 | "type": "boolean", 219 | "default": true, 220 | "description": "Show command in context menu: \"Move to Right\"." 221 | }, 222 | "markdowntable.formatOnSave": { 223 | "type": "boolean", 224 | "default": false, 225 | "description": "Format all tables on save." 226 | } 227 | } 228 | } 229 | }, 230 | "scripts": { 231 | "vscode:prepublish": "npm run compile", 232 | "compile": "tsc -p ./", 233 | "lint": "eslint src --ext ts", 234 | "watch": "tsc -watch -p ./", 235 | "pretest": "npm run compile && npm run lint", 236 | "test": "node ./out/test/runTest.js" 237 | }, 238 | "devDependencies": { 239 | "@types/mocha": "^10.0.10", 240 | "@types/node": "^22.10.7", 241 | "@types/vscode": "^1.80.0", 242 | "@typescript-eslint/eslint-plugin": "^8.20.0", 243 | "@typescript-eslint/parser": "^8.20.0", 244 | "eslint": "^9.18.0", 245 | "glob": "^11.0.1", 246 | "mocha": "^11.0.1", 247 | "typescript": "^5.7.3", 248 | "vscode-test": "^1.6.1" 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/markdownTableDataHelper.ts: -------------------------------------------------------------------------------- 1 | import { workspace } from "vscode"; 2 | import MarkdownTableData from './markdownTableData'; 3 | import * as Utility from './markdownTableUtility'; 4 | 5 | 6 | /** 7 | * テーブルを表すマークダウンテキストを MarkdownTableData に変換する 8 | * @param tableText テーブルを表すマークダウンテキスト 9 | */ 10 | export function stringToTableData(tableText: string): MarkdownTableData { 11 | const lines = tableText.split(/\r\n|\n|\r/); 12 | 13 | let getIndent = (linestr: string) => { 14 | if (linestr.trim().startsWith('|')) { 15 | let linedatas = linestr.split('|'); 16 | return linedatas[0]; 17 | } 18 | else { 19 | return ''; 20 | } 21 | }; 22 | 23 | // 1行目 24 | const columns = Utility.splitline(lines[0], 0); 25 | const columnNum = columns.length; 26 | const indent = getIndent(lines[0]); 27 | 28 | // 2行目の寄せ記号 29 | let aligns: [string, string][] = new Array(); 30 | let alignTexts: string[] = new Array(); 31 | const aligndatas = Utility.splitline(lines[1], columnNum, '---'); 32 | for (let i = 0; i < columnNum; i++) { 33 | alignTexts[i] = aligndatas[i]; 34 | let celldata = aligndatas[i].trim(); 35 | aligns[i] = [celldata[0], celldata.slice(-1)]; 36 | } 37 | 38 | // セルの値を取得 39 | const cells: string[][] = new Array(); 40 | const leftovers: string[] = new Array(); 41 | let cellrow = -1; 42 | for (let row = 2; row < lines.length; row++) { 43 | cellrow++; 44 | 45 | const linedatas = Utility.splitline(lines[row], columnNum); 46 | cells[cellrow] = linedatas.slice(0, columnNum); 47 | 48 | // あまりデータを収集する 49 | leftovers[cellrow] = ''; 50 | if (linedatas.length > columnNum) { 51 | const leftoverdatas = linedatas.slice(columnNum, linedatas.length); 52 | leftovers[cellrow] = leftoverdatas.join('|'); 53 | } 54 | } 55 | 56 | return new MarkdownTableData(tableText, aligns, alignTexts, columns, cells, leftovers, indent); 57 | } 58 | 59 | function CreateMarkdownTableData(_text: string, _aligns: [string, string][], _columns: string[], _cells: string[][], _leftovers: string[], _indent: string): MarkdownTableData { 60 | let alignTexts: string[] = new Array(); 61 | for (let column = 0; column < _aligns.length; column++) { 62 | alignTexts[column] = _aligns[column][0] + '-' + _aligns[column][1]; 63 | } 64 | return new MarkdownTableData(_text, _aligns, alignTexts, _columns, _cells, _leftovers, _indent); 65 | } 66 | 67 | function convertSeparatedValuesToTableData(text: string, separater: string): MarkdownTableData { 68 | // 入力データを行ごとに分割する 69 | let lines = text.split(/\r\n|\n|\r/); 70 | // カラムデータ 71 | let columns: string[] = new Array(); 72 | let columntexts = lines[0].split(separater); 73 | // カラム数 74 | let columnCount = columntexts.length; 75 | 76 | for (let i = 0; i < columnCount; i++) { 77 | columns[i] = columntexts[i].trim(); 78 | } 79 | 80 | // 入力データから改行とタブで分割した2次元配列を生成する 81 | let cells: string[][] = new Array(); 82 | // カラム数よりもはみ出たデータ 83 | let leftovers: string[] = new Array(); 84 | for (let row = 1; row < lines.length; row++) { 85 | // 各セルの値 86 | cells[row - 1] = new Array(); 87 | // 行内のデータが足りない場合に備えて空白文字で埋める 88 | for (let column = 0; column < columnCount; column++) { 89 | cells[row - 1][column] = ' '; 90 | } 91 | 92 | // 余りデータを初期化 93 | leftovers[row - 1] = ''; 94 | 95 | // 行データをタブで分割 96 | let lineValues = lines[row].split(separater); 97 | 98 | // 実際の値に置き換える 99 | for (let column = 0; column < lineValues.length; column++) { 100 | if (column >= columnCount) { 101 | // カラムヘッダーよりも多い場合ははみ出しデータ配列に保存 102 | leftovers[row - 1] += separater + lineValues[column]; 103 | continue; 104 | } 105 | cells[row - 1][column] = lineValues[column].trim(); 106 | } 107 | } 108 | 109 | // 表の寄せ記号 110 | let aligns: [string, string][] = new Array(); 111 | for (let column = 0; column < columnCount; column++) { 112 | // 全部左寄せ 113 | aligns[column] = [':', '-']; 114 | } 115 | 116 | const table = CreateMarkdownTableData("", aligns, columns, cells, leftovers, ''); 117 | return CreateMarkdownTableData(toFormatTableStr(table), aligns, columns, cells, leftovers, ''); 118 | } 119 | 120 | /** 121 | * タブ区切りテキスト(TSV)を MarkdownTableData に変換する 122 | * @param tableText タブ区切りテキスト 123 | */ 124 | export function tsvToTableData(tsvText: string): MarkdownTableData { 125 | return convertSeparatedValuesToTableData(tsvText, '\t'); 126 | } 127 | 128 | /** 129 | * カンマ区切りテキスト(CSV)を MarkdownTableData に変換する 130 | * @param tableText タブ区切りテキスト 131 | */ 132 | export function csvToTableData(csvText: string): MarkdownTableData { 133 | return convertSeparatedValuesToTableData(csvText, ','); 134 | } 135 | 136 | /** 137 | * MarkdownTableData に行を追加 138 | * @param tableData 139 | * @param insertAt 140 | * @returns 141 | */ 142 | export function insertRow(tableData: MarkdownTableData, insertAt: number): MarkdownTableData { 143 | const columns = tableData.columns; 144 | const aligns = tableData.aligns; 145 | const cells = tableData.cells; 146 | const leftovers = tableData.leftovers; 147 | const column_num = tableData.columns.length; 148 | const indent = tableData.indent; 149 | 150 | cells.splice(insertAt, 0, Array.from({ length: column_num }, () => ' ')); 151 | leftovers.splice(insertAt, 0, ''); 152 | 153 | const text = tableData.originalText + '\n' + tableData.indent + '|' + ' |'.repeat(tableData.columns.length); 154 | 155 | return CreateMarkdownTableData(text, aligns, columns, cells, leftovers, indent); 156 | } 157 | 158 | export function insertColumn(tableData: MarkdownTableData, insertAt: number): MarkdownTableData { 159 | let columns = tableData.columns; 160 | let aligns = tableData.aligns; 161 | let cells = tableData.cells; 162 | let leftovers = tableData.leftovers; 163 | let column_num = tableData.columns.length; 164 | let indent = tableData.indent; 165 | 166 | columns.splice(insertAt, 0, ''); 167 | aligns.splice(insertAt, 0, ['-', '-']); 168 | for (let i = 0; i < cells.length; i++) { 169 | cells[i].splice(insertAt, 0, ''); 170 | } 171 | 172 | const table = CreateMarkdownTableData("", aligns, columns, cells, leftovers, indent); 173 | return CreateMarkdownTableData(toFormatTableStr(table), aligns, columns, cells, leftovers, indent); 174 | } 175 | 176 | /** 177 | * 各列の最大文字数を調べる 178 | * @param tableData テーブルデータ 179 | * @returns 180 | */ 181 | export function getColumnMaxWidths(tableData: MarkdownTableData): number[] { 182 | let columnNum = tableData.columns.length; 183 | 184 | // 各列の最大文字数を調べる 185 | let maxWidths: number[] = new Array(); 186 | // コラムヘッダーの各項目の文字数 187 | for (let i = 0; i < tableData.columns.length; i++) { 188 | let cellLength = Utility.getLen(tableData.columns[i].trim()); 189 | // 表の寄せ記号行は最短で半角3文字なので、各セル最低でも半角3文字 190 | maxWidths[i] = (3 > cellLength) ? 3 : cellLength; 191 | } 192 | 193 | for (let row = 0; row < tableData.cells.length; row++) { 194 | let cells = tableData.cells[row]; 195 | for (let i = 0; i < cells.length; i++) { 196 | if (i > columnNum) { break; } 197 | let cellLength = Utility.getLen(cells[i].trim()); 198 | maxWidths[i] = (maxWidths[i] > cellLength) ? maxWidths[i] : cellLength; 199 | } 200 | } 201 | 202 | return maxWidths; 203 | } 204 | 205 | export function toFormatTableStr(tableData: MarkdownTableData): string { 206 | const alignData = workspace.getConfiguration('markdowntable').get('alignData'); 207 | const alignHeader = workspace.getConfiguration('markdowntable').get('alignColumnHeader'); 208 | const paddedDelimiterRowPipes = workspace.getConfiguration('markdowntable').get('paddedDelimiterRowPipes'); 209 | 210 | // 各列の最大文字数を調べる 211 | const maxWidths = getColumnMaxWidths(tableData); 212 | 213 | 214 | 215 | const columnNum = tableData.columns.length; 216 | const formatted: string[] = new Array(); 217 | 218 | // 列幅をそろえていく 219 | for (let row = 0; row < tableData.cells.length; row++) { 220 | formatted[row] = ''; 221 | formatted[row] += tableData.indent; 222 | const cells = tableData.cells[row]; 223 | for (let i = 0; i < columnNum; i++) { 224 | let celldata = ''; 225 | if (i < cells.length) { 226 | celldata = cells[i].trim(); 227 | } 228 | const celldata_length = Utility.getLen(celldata); 229 | 230 | // | の後にスペースを入れる 231 | formatted[row] += '| '; 232 | if (alignData) { 233 | let [front, end] = tableData.aligns[i]; 234 | if (front === ':' && end === ':') { 235 | // 中央ぞろえ 236 | for (let n = 0; n < (maxWidths[i] - celldata_length) / 2 - 0.5; n++) { 237 | formatted[row] += ' '; 238 | } 239 | formatted[row] += celldata; 240 | for (let n = 0; n < (maxWidths[i] - celldata_length) / 2; n++) { 241 | formatted[row] += ' '; 242 | } 243 | } 244 | else if (front === '-' && end === ':') { 245 | // 右揃え 246 | for (let n = 0; n < maxWidths[i] - celldata_length; n++) { 247 | formatted[row] += ' '; 248 | } 249 | formatted[row] += celldata; 250 | } 251 | else { 252 | // 左揃え 253 | formatted[row] += celldata; 254 | for (let n = 0; n < maxWidths[i] - celldata_length; n++) { 255 | formatted[row] += ' '; 256 | } 257 | } 258 | } 259 | else { 260 | // データ 261 | formatted[row] += celldata; 262 | // 余白を半角スペースで埋める 263 | for (let n = celldata_length; n < maxWidths[i]; n++) { 264 | formatted[row] += ' '; 265 | } 266 | } 267 | // | の前にスペースを入れる 268 | formatted[row] += ' '; 269 | } 270 | formatted[row] += '|'; 271 | 272 | // あまりデータを末尾に着ける 273 | if (tableData.leftovers[row].length > 0) { 274 | formatted[row] += tableData.leftovers[row]; 275 | } 276 | } 277 | 278 | // 1行目を成形する 279 | let columnHeader = ''; 280 | columnHeader += tableData.indent; 281 | for (let i = 0; i < columnNum; i++) { 282 | const columnText = tableData.columns[i].trim(); 283 | const columnHeader_length = Utility.getLen(columnText); 284 | 285 | columnHeader += '| '; 286 | if (alignHeader) { 287 | const [front, end] = tableData.aligns[i]; 288 | if (front === ':' && end === ':') { 289 | // 中央ぞろえ 290 | for (let n = 0; n < (maxWidths[i] - columnHeader_length) / 2 - 0.5; n++) { 291 | columnHeader += ' '; 292 | } 293 | columnHeader += columnText; 294 | for (let n = 0; n < (maxWidths[i] - columnHeader_length) / 2; n++) { 295 | columnHeader += ' '; 296 | } 297 | } 298 | else if (front === '-' && end === ':') { 299 | // 右揃え 300 | for (let n = 0; n < maxWidths[i] - columnHeader_length; n++) { 301 | columnHeader += ' '; 302 | } 303 | columnHeader += columnText; 304 | } 305 | else { 306 | // 左揃え 307 | columnHeader += columnText; 308 | for (let n = 0; n < maxWidths[i] - columnHeader_length; n++) { 309 | columnHeader += ' '; 310 | } 311 | } 312 | 313 | } 314 | else { 315 | columnHeader += columnText; 316 | // 余白を-で埋める 317 | for (let n = columnHeader_length; n < maxWidths[i]; n++) { 318 | columnHeader += ' '; 319 | } 320 | } 321 | columnHeader += ' '; 322 | } 323 | columnHeader += '|'; 324 | 325 | 326 | // 2行目を成形する 327 | for (let i = 0; i < columnNum; i++) { 328 | const [front, end] = tableData.aligns[i]; 329 | if (paddedDelimiterRowPipes) { 330 | tableData.alignTexts[i] = ' ' + front; 331 | } else { 332 | tableData.alignTexts[i] = front + '-'; 333 | } 334 | // 余白を-で埋める 335 | for (let n = 1; n < maxWidths[i] - 1; n++) { 336 | tableData.alignTexts[i] += '-'; 337 | } 338 | if (paddedDelimiterRowPipes) { 339 | tableData.alignTexts[i] += end + ' '; 340 | } else { 341 | tableData.alignTexts[i] += '-' + end; 342 | } 343 | } 344 | let tablemark = ''; 345 | tablemark += tableData.indent; 346 | for (let i = 0; i < tableData.alignTexts.length; i++) { 347 | const alignText = tableData.alignTexts[i]; 348 | tablemark += '|' + alignText; 349 | } 350 | tablemark += '|'; 351 | 352 | formatted.splice(0, 0, columnHeader); 353 | formatted.splice(1, 0, tablemark); 354 | 355 | return formatted.join('\r\n'); 356 | } 357 | 358 | 359 | 360 | // return [line, character] 361 | export function getPositionOfCell(tableData: MarkdownTableData, cellRow: number, cellColumn: number): [number, number] { 362 | const line = (cellRow <= 0) ? 0 : cellRow; 363 | 364 | const lines = tableData.originalText.split(/\r\n|\n|\r/); 365 | const linestr = lines[cellRow]; 366 | 367 | const cells = Utility.splitline(linestr, tableData.columns.length); 368 | 369 | let character = 0; 370 | character += tableData.indent.length; 371 | character += 1; 372 | for (let i = 0; i < cellColumn; i++) { 373 | character += cells[i].length; 374 | character += 1; 375 | } 376 | 377 | return [line, character]; 378 | } 379 | 380 | // return [row, column] 381 | export function getCellAtPosition(tableData: MarkdownTableData, line: number, character: number): [number, number] { 382 | const row = (line <= 0) ? 0 : line; 383 | 384 | const lines = tableData.originalText.split(/\r\n|\n|\r/); 385 | const linestr = lines[row]; 386 | 387 | const cells = Utility.splitline(linestr, tableData.columns.length); 388 | 389 | let column = -1; 390 | let cell_end = tableData.indent.length; 391 | for (let cell of cells) { 392 | column++; 393 | cell_end += 1 + cell.length; 394 | 395 | if (character <= cell_end) { 396 | break; 397 | } 398 | } 399 | 400 | return [row, column]; 401 | } 402 | 403 | export function getCellData(tableData: MarkdownTableData, cellRow: number, cellColumn: number): string { 404 | if (cellRow === 0) { 405 | return (tableData.columns.length > cellColumn) ? tableData.columns[cellColumn] : ""; 406 | } 407 | if (cellRow === 1) { 408 | return (tableData.alignTexts.length > cellColumn) ? tableData.alignTexts[cellColumn] : ""; 409 | } 410 | if (cellRow >= tableData.cells.length + 2) { 411 | return ""; 412 | } 413 | 414 | return tableData.cells[cellRow - 2][cellColumn]; 415 | } 416 | 417 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as mtdh from './markdownTableDataHelper'; 3 | import MarkdownTableData from './markdownTableData'; 4 | import * as text from './textUtility'; 5 | 6 | 7 | export function navigateNextCell(withFormat: boolean) { 8 | console.log('navigateNextCell called!'); 9 | 10 | // エディタ取得 11 | const editor = vscode.window.activeTextEditor as vscode.TextEditor; 12 | // ドキュメント取得 13 | const doc = editor.document; 14 | // 選択範囲取得 15 | const cur_selection = editor.selection; 16 | 17 | // 表を探す 18 | const tableRange = text.findTableRange(doc.getText(), cur_selection.anchor.line, cur_selection.anchor.line); 19 | if (!tableRange) { 20 | return; 21 | } 22 | const [startLine, endLine] = tableRange; 23 | const table_selection = new vscode.Selection(startLine, 0, endLine, 10000); 24 | const table_text = doc.getText(table_selection); 25 | 26 | // 元のカーソル位置を取得 27 | const [prevline, prevcharacter] = [cur_selection.active.line - startLine, cur_selection.active.character]; 28 | 29 | // テーブルをTableDataにシリアライズ 30 | let tableData = mtdh.stringToTableData(table_text); 31 | if (tableData.aligns[0][0] === undefined) { 32 | return; 33 | } 34 | 35 | // 元のカーソル位置のセルを取得 36 | const [prevRow, prevColumn] = mtdh.getCellAtPosition(tableData, prevline, prevcharacter); 37 | 38 | // 次のセルが新しい行になるかどうか 39 | const isNextRow = (prevColumn + 1 >= tableData.columns.length); 40 | const isInsertNewRow = ( 41 | // カラム行、または寄せ記号行の場合は3行目を作成する 42 | (prevRow <= 1 && tableData.cells.length === 0) || 43 | // 現在の行が最終行で、かつ次の行に進む場合は末尾に1行追加する 44 | (isNextRow && prevRow >= tableData.cells.length + 1) 45 | ); 46 | 47 | 48 | // 次の行が必要なら追加する 49 | if (isInsertNewRow === true) { 50 | tableData = mtdh.insertRow(tableData, tableData.cells.length); 51 | } 52 | 53 | // テーブルをフォーマットしたテキストを取得 54 | const new_text = withFormat ? mtdh.toFormatTableStr(tableData) : tableData.originalText; 55 | const tableDataFormatted = mtdh.stringToTableData(new_text); 56 | 57 | //エディタ選択範囲にテキストを反映 58 | editor.edit(edit => { 59 | edit.replace(table_selection, new_text); 60 | }); 61 | 62 | // 新しいカーソル位置を計算 63 | // character の +1 は表セル内の|とデータの間の半角スペース分 64 | const newColumn = (isNextRow === true) ? 0 : prevColumn + 1; 65 | const newRow = (isNextRow === true) ? prevRow + 1 : prevRow; 66 | const [newCellLineAt, newCellCharacterAt] = mtdh.getPositionOfCell(tableDataFormatted, newRow, newColumn); 67 | const nextCellData = mtdh.getCellData(tableDataFormatted, newRow, newColumn); 68 | if (nextCellData.trim() === '') { 69 | const newPositionStart = new vscode.Position( 70 | table_selection.start.line + newCellLineAt, 71 | table_selection.start.character + newCellCharacterAt + 1); 72 | const newPositionEnd = new vscode.Position( 73 | table_selection.start.line + newCellLineAt, 74 | table_selection.start.character + newCellCharacterAt + nextCellData.length - 1); 75 | const newSelection = new vscode.Selection(newPositionEnd, newPositionStart); 76 | 77 | // カーソル位置を移動 78 | editor.selection = newSelection; 79 | } 80 | else { 81 | const leftSpaceNum = nextCellData.length - nextCellData.trimLeft().length; 82 | const newPositionStart = new vscode.Position( 83 | table_selection.start.line + newCellLineAt, 84 | table_selection.start.character + newCellCharacterAt + leftSpaceNum); 85 | const newPositionEnd = new vscode.Position( 86 | table_selection.start.line + newCellLineAt, 87 | table_selection.start.character + newCellCharacterAt + leftSpaceNum + nextCellData.trim().length); 88 | const newSelection = new vscode.Selection(newPositionEnd, newPositionStart); 89 | 90 | // カーソル位置を移動 91 | editor.selection = newSelection; 92 | } 93 | }; 94 | 95 | export function navigatePrevCell(withFormat: boolean) { 96 | console.log('navigatePrevCell called!'); 97 | 98 | // エディタ取得 99 | const editor = vscode.window.activeTextEditor as vscode.TextEditor; 100 | // ドキュメント取得 101 | const doc = editor.document; 102 | // 選択範囲取得 103 | const cur_selection = editor.selection; 104 | 105 | // 表を探す 106 | const tableRange = text.findTableRange(doc.getText(), cur_selection.anchor.line, cur_selection.anchor.line); 107 | if (!tableRange) { 108 | return; 109 | } 110 | const [startLine, endLine] = tableRange; 111 | const table_selection = new vscode.Selection(startLine, 0, endLine, 10000); 112 | const table_text = doc.getText(table_selection); 113 | 114 | // 元のカーソル位置を取得 115 | const [prevline, prevcharacter] = [cur_selection.active.line - startLine, cur_selection.active.character]; 116 | 117 | // テーブルをTableDataにシリアライズ 118 | let tableData = mtdh.stringToTableData(table_text); 119 | if (tableData.aligns[0][0] === undefined) { 120 | return; 121 | } 122 | 123 | // 元のカーソル位置のセルを取得 124 | const [prevRow, prevColumn] = mtdh.getCellAtPosition(tableData, prevline, prevcharacter); 125 | // 先頭セルだったら何もしない 126 | if (prevColumn <= 0 && prevRow <= 0) { 127 | return; 128 | } 129 | 130 | // テーブルをフォーマットしたテキストを取得 131 | const new_text = withFormat ? mtdh.toFormatTableStr(tableData) : tableData.originalText; 132 | const tableDataFormatted = mtdh.stringToTableData(new_text); 133 | 134 | //エディタ選択範囲にテキストを反映 135 | editor.edit(edit => { 136 | edit.replace(table_selection, new_text); 137 | }); 138 | 139 | // 新しいカーソル位置を計算 140 | // character の +1 は表セル内の|とデータの間の半角スペース分 141 | const newColumn = (prevColumn > 0) ? prevColumn - 1 : tableDataFormatted.columns.length - 1; 142 | const newRow = (prevColumn > 0) ? prevRow : prevRow - 1; 143 | const [newCellLineAt, newCellCharacterAt] = mtdh.getPositionOfCell(tableDataFormatted, newRow, newColumn); 144 | const nextCellData = mtdh.getCellData(tableDataFormatted, newRow, newColumn); 145 | if (nextCellData.trim() === '') { 146 | const newPositionStart = new vscode.Position( 147 | table_selection.start.line + newCellLineAt, 148 | table_selection.start.character + newCellCharacterAt + 1); 149 | const newPositionEnd = new vscode.Position( 150 | table_selection.start.line + newCellLineAt, 151 | table_selection.start.character + newCellCharacterAt + nextCellData.length - 1); 152 | const newSelection = new vscode.Selection(newPositionEnd, newPositionStart); 153 | 154 | // カーソル位置を移動 155 | editor.selection = newSelection; 156 | } 157 | else { 158 | const leftSpaceNum = nextCellData.length - nextCellData.trimLeft().length; 159 | const newPositionStart = new vscode.Position( 160 | table_selection.start.line + newCellLineAt, 161 | table_selection.start.character + newCellCharacterAt + leftSpaceNum); 162 | const newPositionEnd = new vscode.Position( 163 | table_selection.start.line + newCellLineAt, 164 | table_selection.start.character + newCellCharacterAt + leftSpaceNum + nextCellData.trim().length); 165 | const newSelection = new vscode.Selection(newPositionEnd, newPositionStart); 166 | 167 | // カーソル位置を移動 168 | editor.selection = newSelection; 169 | } 170 | 171 | }; 172 | 173 | export function formatAll() { 174 | // エディタ取得 175 | const editor = vscode.window.activeTextEditor as vscode.TextEditor; 176 | // ドキュメント取得 177 | const doc = editor.document; 178 | // 変換のリスト 179 | let format_list = [] as [vscode.Selection, MarkdownTableData][]; 180 | const alltext = doc.getText(); 181 | 182 | // 表を探す 183 | let preSearchedLine = -1; 184 | for (let line = 0; line < doc.lineCount; line++) { 185 | if (line <= preSearchedLine) { 186 | continue; 187 | } 188 | if (!text.isInTable(alltext, line, line)) { 189 | continue; 190 | } 191 | 192 | // 表の終わり行を探す 193 | const tableRange = text.findTableRange(doc.getText(), line, line); 194 | if (!tableRange) { 195 | continue; 196 | } 197 | const [startLine, endLine] = tableRange; 198 | 199 | // 表のテキストを取得 200 | const table_selection = new vscode.Selection(startLine, 0, endLine, doc.lineAt(endLine).text.length); 201 | const table_text = doc.getText(table_selection); 202 | 203 | // 表をフォーマットする 204 | const tableData = mtdh.stringToTableData(table_text); 205 | 206 | // 変換内容をリストに保持する 207 | format_list.push([table_selection, tableData]); 208 | 209 | preSearchedLine = endLine; 210 | } 211 | 212 | // 新しいカーソル位置(editor.editでの処理が完了してから動かさないとずれるため外に置く) 213 | let newSelection = new vscode.Selection(editor.selection.active, editor.selection.active); 214 | 215 | //エディタ選択範囲にテキストを反映 216 | editor.edit(edit => { 217 | for (let i = 0; i < format_list.length; i++) { 218 | const [selection, tableData] = format_list[i] as [vscode.Selection, MarkdownTableData]; 219 | 220 | // カーソルを元のセルと同じ位置にするためにカーソル位置を特定しておく 221 | if (selection.contains(editor.selection.active)) { 222 | // テーブルの変形処理クラス 223 | const [prevline, prevcharacter] = [editor.selection.active.line - selection.start.line, editor.selection.active.character]; 224 | const [prevRow, prevColumn] = mtdh.getCellAtPosition(tableData, prevline, prevcharacter); 225 | 226 | // テキストを置換 227 | const tableStrFormatted = mtdh.toFormatTableStr(tableData); 228 | const tableDataFormatted = mtdh.stringToTableData(tableStrFormatted); 229 | 230 | edit.replace(selection, tableStrFormatted); 231 | 232 | // 新しいカーソル位置を計算 233 | // character の +1 は表セル内の|とデータの間の半角スペース分 234 | const [newline, newcharacter] = mtdh.getPositionOfCell(tableDataFormatted, prevRow, prevColumn); 235 | const newPosition = new vscode.Position( 236 | selection.start.line + newline, 237 | selection.start.character + newcharacter + 1); 238 | newSelection = new vscode.Selection(newPosition, newPosition); 239 | } 240 | else { 241 | // テキストを置換 242 | edit.replace(selection, mtdh.toFormatTableStr(tableData)); 243 | } 244 | } 245 | }); 246 | 247 | // カーソル位置を移動 248 | editor.selection = newSelection; 249 | } 250 | 251 | export function tsvToTable() { 252 | // エディタ取得 253 | const editor = vscode.window.activeTextEditor as vscode.TextEditor; 254 | // ドキュメント取得 255 | const doc = editor.document; 256 | // 選択範囲取得 257 | const cur_selection = editor.selection; 258 | if (editor.selection.isEmpty) { 259 | return; 260 | } 261 | 262 | const text = doc.getText(cur_selection); //取得されたテキスト 263 | 264 | const tableData = mtdh.tsvToTableData(text); 265 | const newTableStr = mtdh.toFormatTableStr(tableData); 266 | 267 | //エディタ選択範囲にテキストを反映 268 | editor.edit(edit => { 269 | edit.replace(cur_selection, newTableStr); 270 | }); 271 | } 272 | 273 | export function csvToTable() { 274 | // エディタ取得 275 | const editor = vscode.window.activeTextEditor as vscode.TextEditor; 276 | // ドキュメント取得 277 | const doc = editor.document; 278 | // 選択範囲取得 279 | const cur_selection = editor.selection; 280 | if (editor.selection.isEmpty) { 281 | return; 282 | } 283 | 284 | const text = doc.getText(cur_selection); //取得されたテキスト 285 | 286 | const tableData = mtdh.csvToTableData(text); 287 | const newTableStr = mtdh.toFormatTableStr(tableData); 288 | 289 | //エディタ選択範囲にテキストを反映 290 | editor.edit(edit => { 291 | edit.replace(cur_selection, newTableStr); 292 | }); 293 | } 294 | 295 | export function insertColumn(isLeft: boolean) { 296 | // エディタ取得 297 | const editor = vscode.window.activeTextEditor as vscode.TextEditor; 298 | // ドキュメント取得 299 | const doc = editor.document; 300 | // 選択範囲取得 301 | const cur_selection = editor.selection; 302 | 303 | // 表を探す 304 | const tableRange = text.findTableRange(doc.getText(), cur_selection.active.line, cur_selection.active.line); 305 | if (!tableRange) { 306 | return; 307 | } 308 | const [startLine, endLine] = tableRange; 309 | const table_selection = new vscode.Selection(startLine, 0, endLine, 10000); 310 | const table_text = doc.getText(table_selection); 311 | 312 | 313 | // 元のカーソル位置を取得 314 | const [prevline, prevcharacter] = [cur_selection.active.line - startLine, cur_selection.active.character]; 315 | 316 | // テーブルをフォーマット 317 | const tableData = mtdh.stringToTableData(table_text); 318 | 319 | // 元のカーソル位置のセルを取得 320 | const [prevRow, prevColumn] = mtdh.getCellAtPosition(tableData, prevline, prevcharacter); 321 | 322 | // 挿入位置 323 | const insertPosition = isLeft ? prevColumn : prevColumn + 1; 324 | 325 | const newTableData = mtdh.insertColumn(tableData, insertPosition); 326 | const tableStrFormatted = mtdh.toFormatTableStr(newTableData); 327 | const tableDataFormatted = mtdh.stringToTableData(tableStrFormatted); 328 | 329 | //エディタ選択範囲にテキストを反映 330 | editor.edit(edit => { 331 | edit.replace(table_selection, tableStrFormatted); 332 | }); 333 | 334 | // 新しいカーソル位置を計算 335 | // character の +1 は表セル内の|とデータの間の半角スペース分 336 | const newColumn = insertPosition; 337 | const [newline, newcharacter] = mtdh.getPositionOfCell(tableDataFormatted, prevRow, newColumn); 338 | const newPosition = new vscode.Position( 339 | table_selection.start.line + newline, 340 | table_selection.start.character + newcharacter + 1); 341 | const newSelection = new vscode.Selection(newPosition, newPosition); 342 | 343 | // カーソル位置を移動 344 | editor.selection = newSelection; 345 | }; 346 | 347 | export function alignColumns(alignMark: [string, string]) { 348 | // エディタ取得 349 | const editor = vscode.window.activeTextEditor as vscode.TextEditor; 350 | // ドキュメント取得 351 | const doc = editor.document; 352 | // 選択範囲取得 353 | const cur_selection = editor.selection; 354 | 355 | // 選択範囲を含むテーブルを探す 356 | const tableRange = text.findTableRange(doc.getText(), cur_selection.start.line, cur_selection.start.line); 357 | if (!tableRange) { 358 | // テーブル内ではなかったら終了 359 | vscode.window.showErrorMessage('Markdown Table : Align command failed, because your selection is not inside of a table.'); 360 | return; 361 | } 362 | const [startLine, endLine] = tableRange; 363 | const table_selection = new vscode.Selection(startLine, 0, endLine, 10000); 364 | const table_text = doc.getText(table_selection); 365 | 366 | 367 | // テーブルをTableDataにシリアライズ 368 | let tableData = mtdh.stringToTableData(table_text); 369 | if (tableData.aligns[0][0] === undefined) { 370 | return; 371 | } 372 | 373 | // 選択セルを取得 374 | const [startline, startcharacter] = [cur_selection.start.line - startLine, cur_selection.start.character]; 375 | const [startRow, startColumn] = mtdh.getCellAtPosition(tableData, startline, startcharacter); 376 | const [endline, endcharacter] = [cur_selection.end.line - startLine, cur_selection.end.character]; 377 | const [endRow, endColumn] = mtdh.getCellAtPosition(tableData, endline, endcharacter); 378 | 379 | // 選択範囲の列のAlignを変更する 380 | if (startRow === endRow) { 381 | // 選択範囲の開始位置と終了位置が同じ行内の場合 382 | for (let column = startColumn; column <= endColumn; column++) { 383 | tableData.aligns[column] = alignMark; 384 | } 385 | } 386 | else if (startRow + 1 === endRow) { 387 | // 選択範囲が2行にまたがる場合 388 | for (let column = startColumn; column <= tableData.columns.length; column++) { 389 | tableData.aligns[column] = alignMark; 390 | } 391 | for (let column = 0; column <= endColumn; column++) { 392 | tableData.aligns[column] = alignMark; 393 | } 394 | } 395 | else { 396 | // 選択範囲が3行以上にまたがる場合はすべての列が対象 397 | for (let column = 0; column < tableData.columns.length; column++) { 398 | tableData.aligns[column] = alignMark; 399 | } 400 | } 401 | 402 | // テーブルをフォーマットした文字列を取得 403 | const newTableText = mtdh.toFormatTableStr(tableData); 404 | 405 | //エディタ選択範囲にテキストを反映 406 | editor.edit(edit => { 407 | edit.replace(table_selection, newTableText); 408 | }); 409 | 410 | // 元のカーソル選択位置を計算 411 | const [anchorline, anchorcharacter] = [cur_selection.anchor.line - startLine, cur_selection.anchor.character]; 412 | // 元のカーソル選択位置のセルを取得 413 | const [anchorRow, anchorColumn] = mtdh.getCellAtPosition(tableData, anchorline, anchorcharacter,); 414 | 415 | const tableStrFormatted = mtdh.toFormatTableStr(tableData); 416 | const tableDataFormatted = mtdh.stringToTableData(tableStrFormatted); 417 | 418 | // 新しいカーソル位置をフォーマット後のテキストから計算 419 | const [newline, newcharacter] = mtdh.getPositionOfCell(tableDataFormatted, anchorRow, anchorColumn); 420 | const newPosition = new vscode.Position( 421 | table_selection.start.line + newline, 422 | table_selection.start.character + newcharacter + 1); 423 | const newSelection = new vscode.Selection(newPosition, newPosition); 424 | 425 | // カーソル位置を移動 426 | editor.selection = newSelection; 427 | }; 428 | 429 | export function moveColumns(toLeft: boolean) { 430 | // エディタ取得 431 | const editor = vscode.window.activeTextEditor as vscode.TextEditor; 432 | // ドキュメント取得 433 | const doc = editor.document; 434 | // 選択範囲取得 435 | const cur_selection = editor.selection; 436 | 437 | // 選択範囲を含むテーブルを探す 438 | const tableRange = text.findTableRange(doc.getText(), cur_selection.start.line, cur_selection.start.line); 439 | if (!tableRange) { 440 | // テーブル内ではなかったら終了 441 | vscode.window.showErrorMessage('Markdown Table : Move command failed, because your selection is not inside of a table.'); 442 | return; 443 | } 444 | const [startLine, endLine] = tableRange; 445 | const table_selection = new vscode.Selection(startLine, 0, endLine, 10000); 446 | const table_text = doc.getText(table_selection); 447 | 448 | // テーブルをTableDataにシリアライズ 449 | let tableData = mtdh.stringToTableData(table_text); 450 | if (tableData.aligns[0][0] === undefined) { 451 | return; 452 | } 453 | 454 | // 選択セルを取得 455 | const [startline, startcharacter] = [cur_selection.start.line - startLine, cur_selection.start.character]; 456 | const [startRow, startColumn] = mtdh.getCellAtPosition(tableData, startline, startcharacter); 457 | const [endline, endcharacter] = [cur_selection.end.line - startLine, cur_selection.end.character]; 458 | const [endRow, endColumn] = mtdh.getCellAtPosition(tableData, endline, endcharacter); 459 | 460 | if (startRow !== endRow || (toLeft && startColumn <= 0) || (!toLeft && endColumn >= tableData.columns.length - 1)) { 461 | // 選択範囲の開始位置と終了位置が同じ行内ではない場合 462 | if (toLeft) { 463 | vscode.window.showErrorMessage('Markdown Table : Move-Left command failed, because your selection is already in the left end.'); 464 | } 465 | else { 466 | vscode.window.showErrorMessage('Markdown Table : Move-Right command failed, because your selection is already in the right end.'); 467 | } 468 | return; 469 | } 470 | 471 | // 配列中の1アイテムを別の位置に移動させるローカル関数 472 | // 配列は参照で処理する 473 | const moveArrayItem = (array: Array, moveFrom: number, moveTo: number): void => { 474 | let itemMoved = array.slice(moveFrom, moveFrom + 1)[0]; 475 | array.splice(moveFrom, 1); 476 | array.splice(moveTo, 0, itemMoved); 477 | }; 478 | 479 | // 選択範囲の列を動かす 480 | // 複数行を左に動かすのは、移動対象範囲の左の1セルのみを対象範囲の右に移動すると読み替える 481 | // 複数行を右に動かすのは、移動対象範囲の右の1セルのみを対象範囲の左に移動すると読み替える 482 | const moveFrom = toLeft ? startColumn - 1 : endColumn + 1; 483 | const moveTo = toLeft ? endColumn : startColumn; 484 | moveArrayItem(tableData.aligns, moveFrom, moveTo); 485 | moveArrayItem(tableData.alignTexts, moveFrom, moveTo); 486 | moveArrayItem(tableData.columns, moveFrom, moveTo); 487 | tableData.cells.forEach(rowCells => { 488 | moveArrayItem(rowCells, moveFrom, moveTo); 489 | }); 490 | 491 | // テーブルをフォーマットした文字列を取得 492 | const newTableText = mtdh.toFormatTableStr(tableData); 493 | 494 | //エディタ選択範囲にテキストを反映 495 | editor.edit(edit => { 496 | edit.replace(table_selection, newTableText); 497 | }); 498 | 499 | // 元のカーソル選択位置を計算 500 | const [anchorline, anchorcharacter] = [cur_selection.anchor.line - startLine, cur_selection.anchor.character]; 501 | // 元のカーソル選択位置のセルを取得 502 | const [anchorRow, anchorColumn] = mtdh.getCellAtPosition(tableData, anchorline, anchorcharacter,); 503 | 504 | const tableStrFormatted = mtdh.toFormatTableStr(tableData); 505 | const tableDataFormatted = mtdh.stringToTableData(tableStrFormatted); 506 | 507 | // 新しいカーソル位置をフォーマット後のテキストから計算 508 | const [newline, newcharacter] = mtdh.getPositionOfCell(tableDataFormatted, anchorRow, anchorColumn); 509 | const newPosition = new vscode.Position( 510 | table_selection.start.line + newline, 511 | table_selection.start.character + newcharacter + 1); 512 | const newSelection = new vscode.Selection(newPosition, newPosition); 513 | 514 | // カーソル位置を移動 515 | editor.selection = newSelection; 516 | }; --------------------------------------------------------------------------------