From 5deb10ab8b89807ba4d845d4b81a55fdac483e57 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Wed, 2 Apr 2025 21:29:48 +0100 Subject: [PATCH] Add cursor position conversions --- .../utils/line-and-column-to-position.test.ts | 43 ++++++++++++ .../src/utils/line-and-column-to-position.ts | 34 +++++++++ .../utils/position-to-line-and-column.test.ts | 69 +++++++++++++++++++ .../src/utils/position-to-line-and-column.ts | 30 ++++++++ 4 files changed, 176 insertions(+) create mode 100644 frontend/obsidian-plugin/src/utils/line-and-column-to-position.test.ts create mode 100644 frontend/obsidian-plugin/src/utils/line-and-column-to-position.ts create mode 100644 frontend/obsidian-plugin/src/utils/position-to-line-and-column.test.ts create mode 100644 frontend/obsidian-plugin/src/utils/position-to-line-and-column.ts diff --git a/frontend/obsidian-plugin/src/utils/line-and-column-to-position.test.ts b/frontend/obsidian-plugin/src/utils/line-and-column-to-position.test.ts new file mode 100644 index 00000000..b98f66e5 --- /dev/null +++ b/frontend/obsidian-plugin/src/utils/line-and-column-to-position.test.ts @@ -0,0 +1,43 @@ +import { lineAndColumnToPosition } from "./line-and-column-to-position"; + +describe("lineAndColumnToPosition", () => { + it("should return the correct position for the first line", () => { + const text = "Hello\nWorld"; + const position = lineAndColumnToPosition(text, 0, 3); + expect(position).toBe(3); + }); + + it("should return the correct position for the second line", () => { + const text = "Hello\nWorld"; + const position = lineAndColumnToPosition(text, 1, 2); + expect(position).toBe(8); + }); + + it("should return the correct position for an empty string", () => { + const text = ""; + const position = lineAndColumnToPosition(text, 0, 0); + expect(position).toBe(0); + }); + + it("should handle a single-line string correctly", () => { + const text = "SingleLine"; + const position = lineAndColumnToPosition(text, 0, 5); + expect(position).toBe(5); + }); + + it("should handle multi-line strings with varying lengths", () => { + const text = "Line1\nLongerLine2\nShort3"; + const position = lineAndColumnToPosition(text, 2, 4); + expect(position).toBe(22); + }); + + it("should throw an error if the line number is out of range", () => { + const text = "Line1\nLine2"; + expect(() => lineAndColumnToPosition(text, 3, 0)).toThrow(); + }); + + it("should throw an error if the column number is out of range", () => { + const text = "Line1\nLine2"; + expect(() => lineAndColumnToPosition(text, 1, 10)).toThrow(); + }); +}); diff --git a/frontend/obsidian-plugin/src/utils/line-and-column-to-position.ts b/frontend/obsidian-plugin/src/utils/line-and-column-to-position.ts new file mode 100644 index 00000000..0bc114c7 --- /dev/null +++ b/frontend/obsidian-plugin/src/utils/line-and-column-to-position.ts @@ -0,0 +1,34 @@ +/** + * Converts line and column coordinates to an absolute character position in a text string. + * + * @param line - The zero-based line number + * @param column - The zero-based column number + * @param text - The text string to calculate position in + * @returns The absolute character position (zero-based index) in the text string + * @throws Error if line number is out of range + * @throws Error if column number is out of range + */ +export function lineAndColumnToPosition( + text: string, + line: number, + column: number +): number { + const lines = text.split("\n"); + + if (line >= lines.length) { + throw new Error(`Line number ${line} is out of range.`); + } + + if (column > lines[line].length) { + throw new Error(`Column number ${column} is out of range.`); + } + + let position = 0; + for (let i = 0; i < line; i++) { + position += lines[i].length + 1; + } + + position += column; + + return position; +} diff --git a/frontend/obsidian-plugin/src/utils/position-to-line-and-column.test.ts b/frontend/obsidian-plugin/src/utils/position-to-line-and-column.test.ts new file mode 100644 index 00000000..e5d3bac5 --- /dev/null +++ b/frontend/obsidian-plugin/src/utils/position-to-line-and-column.test.ts @@ -0,0 +1,69 @@ +import { positionToLineAndColumn } from "./position-to-line-and-column"; + +describe("positionToLineAndColumn", () => { + test("converts position to line and column in a single line text", () => { + const text = "Hello, world!"; + expect(positionToLineAndColumn(text, 0)).toEqual({ + line: 0, + column: 1 + }); + expect(positionToLineAndColumn(text, 7)).toEqual({ + line: 0, + column: 8 + }); + expect(positionToLineAndColumn(text, 12)).toEqual({ + line: 0, + column: 13 + }); + }); + + test("converts position to line and column in multi-line text", () => { + const text = "First line\nSecond line\nThird line"; + expect(positionToLineAndColumn(text, 0)).toEqual({ + line: 0, + column: 1 + }); + expect(positionToLineAndColumn(text, 10)).toEqual({ + line: 0, + column: 11 + }); + expect(positionToLineAndColumn(text, 15)).toEqual({ + line: 1, + column: 5 + }); + expect(positionToLineAndColumn(text, 26)).toEqual({ + line: 2, + column: 4 + }); + }); + + test("handles positions at line breaks", () => { + const text = "Line\nBreak"; + expect(positionToLineAndColumn(text, 4)).toEqual({ + line: 0, + column: 5 + }); + expect(positionToLineAndColumn(text, 5)).toEqual({ + line: 1, + column: 1 + }); + }); + + test("handles empty input", () => { + expect(positionToLineAndColumn("", 0)).toEqual({ line: 0, column: 1 }); + }); + + test("handles positions at the end of text", () => { + const text = "End"; + expect(positionToLineAndColumn(text, 3)).toEqual({ + line: 0, + column: 4 + }); + }); + + test("throws error for position out of range", () => { + const text = "Short text"; + expect(() => positionToLineAndColumn(text, 15)).toThrow(); + expect(() => positionToLineAndColumn(text, -1)).toThrow(); + }); +}); diff --git a/frontend/obsidian-plugin/src/utils/position-to-line-and-column.ts b/frontend/obsidian-plugin/src/utils/position-to-line-and-column.ts new file mode 100644 index 00000000..a9c81881 --- /dev/null +++ b/frontend/obsidian-plugin/src/utils/position-to-line-and-column.ts @@ -0,0 +1,30 @@ +/** + * Converts a character position in text to line and column numbers. + * + * @param text The text content to analyze + * @param position The character position to convert + * @returns An object containing line and column numbers (0-based index for line, 1-based index for column) + * @throws Will throw an error if the position is negative or exceeds the text length + */ +export function positionToLineAndColumn( + text: string, + position: number +): { line: number; column: number } { + if (position < 0) { + throw new Error("Position cannot be negative"); + } + + if (position > text.length) { + throw new Error( + `Position ${position} exceeds text length ${text.length}` + ); + } + + const textUpToPosition = text.substring(0, position); + const lines = textUpToPosition.split("\n"); + + const line = lines.length - 1; // 0-based index + const column = lines[lines.length - 1].length + 1; // 1-based index + + return { line, column }; +}