Skip to content

Commit

Permalink
Merge pull request #91 from gaelj/release
Browse files Browse the repository at this point in the history
Release 0.3.0
  • Loading branch information
gaelj authored Feb 5, 2024
2 parents 294acbe + 9db429c commit 6ac1983
Show file tree
Hide file tree
Showing 18 changed files with 321 additions and 19 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 0.3.0 - 2024-02-06

### ✨ Introduce new features

- Add commands to insert markdown table and horizontal rule
- Implement csv and tsv with padded column widths (Does not support multiline cells)
- Add Tab and Shift-Tab keymaps to csv mode, to navigate columns

### 🎨 Improve structure / format of the code

- Improve eq() of emojiWidget

## 0.2.2 - 2024-02-01

### ⚡️ Improve performance
Expand Down
2 changes: 1 addition & 1 deletion CodeMirror6/CodeMirror6.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<AssemblyName>GaelJ.BlazorCodeMirror6</AssemblyName>
<IsPackable>true</IsPackable>
<PackageId>GaelJ.BlazorCodeMirror6</PackageId>
<Version>0.2.2</Version>
<Version>0.3.0</Version>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Expand Down
9 changes: 9 additions & 0 deletions CodeMirror6/Commands/CMCommandDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,13 @@ public class CMCommandDispatcher
/// <param name="value"></param>
/// <returns></returns>
public Task Dispatch<TValue>(CodeMirrorCommandOneParameter command, TValue value) => cmJsInterop.ModuleInvokeVoidAsync("dispatchCommand", command, value);

/// <summary>
/// Invoke a built-in CodeMirror command with two parameters
/// </summary>
/// <param name="command"></param>
/// <param name="value1"></param>
/// <param name="value2"></param>
/// <returns></returns>
public Task Dispatch<TValue1, TValue2>(CodeMirrorCommandTwoParameters command, TValue1 value1, TValue2 value2) => cmJsInterop.ModuleInvokeVoidAsync("dispatchCommand", command, value1, value2);
}
15 changes: 15 additions & 0 deletions CodeMirror6/Models/CodeMirrorCommandTwoParameters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace GaelJ.BlazorCodeMirror6.Models;

/// <summary>
/// Built-in CodeMirror commands expecting 2 parameters
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CodeMirrorCommandTwoParameters
{
/// <summary>
/// Insert a markdown table at the current selection
/// </summary>
InsertTable,
}
10 changes: 10 additions & 0 deletions CodeMirror6/Models/CodeMirrorLanguage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ public enum CodeMirrorLanguage
/// </summary>
[JsonStringValue("Plain Text")] PlainText,

/// <summary>
/// Comma-separated values
/// </summary>
[JsonStringValue("CSV")] Csv,

/// <summary>
/// Tabulation-separated values
/// </summary>
[JsonStringValue("TSV")] Tsv,

/// <summary>
/// APL
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions CodeMirror6/Models/CodeMirrorSimpleCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ public enum CodeMirrorSimpleCommand
/// </summary>
ToggleMarkdownTaskList,

/// <summary>
/// Insert a markdown horizontal rule above the current cursor position
/// </summary>
InsertMarkdownHorizontalRule,

/// <summary>
/// Undo the last change
/// </summary>
Expand Down
211 changes: 211 additions & 0 deletions CodeMirror6/NodeLib/src/CmColumns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { Decoration, ViewPlugin, EditorView, KeyBinding } from "@codemirror/view";
import { Extension, RangeSetBuilder, Transaction } from "@codemirror/state";
import { buildWidget } from "./lib/codemirror-kit";


function createColumnReplaceDecoration(content: string, from: number) {
return Decoration.widget({
widget: buildWidget({
eq: (other) => other.content === content && other.from === from,
toDOM: (view: EditorView) => {
const span = document.createElement("span")
span.setAttribute("aria-hidden", "true")
span.style.whiteSpace = "pre";
span.textContent = content;
span.onclick = () => {
view.dispatch(view.state.update({ selection: { anchor: from } }))
}
return span
},
ignoreEvent: () => false,
content: content,
from: from,
}),
side: 1,
})
}

// get next or previous column offset relative to the current position
function getRelativeColumnOffset(text: string, separator: string, position: number, previous: boolean): number {
let offset = 0
let inQuotes = false
let escapeNext = false
let previousColumnOffset = 0
for (let i = 0; i < text.length; i++) {
if (i === position && !previous)
offset = 0
else if (i === position && previous)
return previousColumnOffset - position
const char = text[i]
if (escapeNext) {
offset++
escapeNext = false
} else if (char === '"' && i < (text.length - 1) && text[i + 1] === '"') {
offset++
escapeNext = true
} else if (char === '"') {
inQuotes = !inQuotes
offset++
} else if (char === '\\') {
escapeNext = true
offset++
} else if (char === separator && !inQuotes && previous) {
previousColumnOffset = offset
offset++
} else if (char === separator && !inQuotes && i >= position) {
return offset
} else {
offset++
}
}
return offset
}

export function columnStylingPlugin(separator: string): Extension {
return ViewPlugin.define((view: EditorView) => {
return {
update: () => {
const atomicDecoration = Decoration.mark({ atomic: true })
const maxWidths = findMaxColumnWidthsInCsv(view.state.doc.toString(), separator)
const builder = new RangeSetBuilder<Decoration>()
for (const { from, to } of view.visibleRanges) {
let pos = from;
while (pos < to) {
const line = view.state.doc.lineAt(pos)
let cell = ""
let remaining = line.text
let index = 0
let cellStartOffset = 0
let paddingSize = 0
if (remaining !== "") {
while (remaining !== null) {
[cell, remaining] = extractNextCell(remaining, separator);
if (index > 0) {
const padding = " ".repeat(paddingSize)
const widget = createColumnReplaceDecoration(padding, line.from + cellStartOffset - 1)
builder.add(line.from + cellStartOffset - 1, line.from + cellStartOffset - 1, widget)
builder.add(line.from + cellStartOffset - 1, line.from + cellStartOffset, atomicDecoration)
}
paddingSize = maxWidths[index] - cell.length + 1
cellStartOffset += cell.length + 1 // For the cell and the comma
index++
}
}
pos = line.to + 1
}
}
return builder.finish()
},
}
},
{
decorations: plugin => plugin.update(),
eventHandlers: {
keydown: (e, view) => {
console.log(e)
if (e.ctrlKey === true || e.metaKey === true || e.altKey === true || e.shiftKey === true)
return
if (e.key === "ArrowLeft") {
moveCursor(view, -1)
e.preventDefault()
}
else if (e.key === "ArrowRight") {
moveCursor(view, 1)
e.preventDefault()
}
}
}
})
}

export const getColumnStylingKeymap = (separator: string): KeyBinding[] => [
{ key: 'Tab', run: (view) => {
const offset = getRelativeColumnOffset(view.state.doc.toString(), separator, view.state.selection.main.anchor, false)
moveCursor(view, offset + 1)
return true
}},
{ key: 'Shift-Tab', run: (view) => {
const offset = getRelativeColumnOffset(view.state.doc.toString(), separator, view.state.selection.main.anchor, true)
moveCursor(view, offset)
return true
}},
]

function moveCursor(view: EditorView, inc: number) {
console.log("moveCursors")
const { state } = view
state.selection.main
const range = state.selection.main
const newAnchor = Math.max(Math.min(state.doc.length, range.anchor + inc), 0)
view.dispatch(state.update({
selection: { anchor: newAnchor },
scrollIntoView: true,
userEvent: 'input'
}))
}


// extract first csv cell from a line of text. Ignore the separator if it is inside quotes. Ignore quotes if they are escaped by another quote. Return the extracted cell and the remaining text after the cell.
function extractNextCell(line: string, separator: string): string[] {
let cell = ""
let inQuotes = false
let escapeNext = false
let separatorFound = false
for (let i = 0; i < line.length; i++) {
const char = line[i]
if (escapeNext) {
cell += char
escapeNext = false
} else if (char === '"' && i < (line.length - 1) && line[i + 1] === '"') {
cell += char
escapeNext = true
} else if (char === '"') {
inQuotes = !inQuotes
cell += char
} else if (char === '\\') {
escapeNext = true
cell += char
} else if (char === separator && !inQuotes) {
separatorFound = true
break
} else {
cell += char
}
}
return [cell, separatorFound === false ? null : line.slice(cell.length + 1)]
}

function extractAllRowCells(line: string, separator: string): string[] {
let remaining = line
let cells = []
while (remaining != null) {
const [cell, newRemaining] = extractNextCell(remaining, separator)
cells.push(cell)
remaining = newRemaining
}
return cells
}

function findMaxColumnWidthsInCsv(csvData: string, separator: string): number[] {
const data = parseCSV(csvData, separator)
return findMaxColumnWidths(data)
}

function findMaxColumnWidths(data: string[][]): number[] {
let maxWidths: number[] = []

data.forEach(row => {
row.forEach((cell, index) => {
const cellWidth = cell.length
if (!maxWidths[index] || cellWidth > maxWidths[index]) {
maxWidths[index] = cellWidth
}
})
})

return maxWidths
}

function parseCSV(csvData: string, separator: string): string[][] {
return csvData.split('\n').map((row) => extractAllRowCells(row, separator))
}
14 changes: 14 additions & 0 deletions CodeMirror6/NodeLib/src/CmCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,20 @@ function modifyHeaderLevelAtSelections(view: EditorView, delta: number): boolean
return true
}

export function insertTableAboveCommand(view: EditorView, x: number, y: number) {
var header = "| Header ".repeat(x) + "|"
var sp = "| ------ ".repeat(x) + "|"
var row = "| ".repeat(x) + "|\n"
const table = `
${header}
${sp}
${row.repeat(y)}
`
insertTextAboveCommand(view, table)
}
export function insertHorizontalRuleAboveCommand(view: EditorView) {
insertTextAboveCommand(view, "\n---\n")
}
export const toggleMarkdownBold: Command = (view: EditorView) => toggleCharactersAroundRanges(view, "**")
export const toggleMarkdownItalic: Command = (view: EditorView) => toggleCharactersAroundRanges(view, "*")
export const toggleMarkdownStrikethrough: Command = (view: EditorView) => toggleCharactersAroundRanges(view, "~~")
Expand Down
3 changes: 2 additions & 1 deletion CodeMirror6/NodeLib/src/CmEmojiView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import { isCursorInRange } from './CmHelpers'
import * as emoji from 'node-emoji'

const emojiWidget = (emoji: string) => buildWidget({
eq: () => false,
eq: (other) => emoji == other.emoji,
toDOM: () => {
const span = document.createElement('span');
span.textContent = emoji
return span;
},
emoji: emoji,
})

export const viewEmojiExtension = (enabled: boolean = true): Extension => {
Expand Down
1 change: 1 addition & 0 deletions CodeMirror6/NodeLib/src/CmInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class CmInstance
public unifiedMergeViewCompartment: Compartment = new Compartment
public highlightTrailingWhitespaceCompartment: Compartment = new Compartment
public highlightWhitespaceCompartment: Compartment = new Compartment
public columnsStylingCompartment: Compartment = new Compartment
}

export const CMInstances: { [id: string]: CmInstance} = {}
4 changes: 3 additions & 1 deletion CodeMirror6/NodeLib/src/CmKeymap.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { toggleMarkdownBold, toggleMarkdownItalic } from "./CmCommands"
import { KeyBinding } from '@codemirror/view';
import { insertTab } from '@codemirror/commands'

export const customMarkdownKeymap = [
export const customMarkdownKeymap: KeyBinding[] = [
{ key: 'Mod-b', run: toggleMarkdownBold }, // Cmd/Ctrl + B for bold
{ key: 'Mod-i', run: toggleMarkdownItalic }, // Cmd/Ctrl + I for italics
]
2 changes: 2 additions & 0 deletions CodeMirror6/NodeLib/src/CmLanguage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export async function getLanguage(languageName: string, fileNameOrExtension: str
console.log("getLanguage: " + languageName)
switch (languageName) {
case "Plain Text":
case "CSV":
case "TSV":
return null
case "Lezer":
return lezer()
Expand Down
Loading

0 comments on commit 6ac1983

Please sign in to comment.