Skip to content

Commit

Permalink
Add on type formatting provider
Browse files Browse the repository at this point in the history
  • Loading branch information
ddaspit committed Nov 1, 2024
1 parent 032ea36 commit 2f63b93
Show file tree
Hide file tree
Showing 27 changed files with 544 additions and 225 deletions.
3 changes: 2 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"${workspaceRoot}/packages/usfm/dist/**/*.cjs"
],
"autoAttachChildProcesses": true,
"preLaunchTask": "npm: dev"
"preLaunchTask": "npm: dev",
"sourceMaps": true
}
]
}
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"scripts": {
"build": "tsup src/index.ts --dts --format cjs,esm --clean --sourcemap",
"dev": "tsup src/index.ts --dts --format cjs,esm --watch",
"dev": "tsup src/index.ts --dts --format cjs,esm --watch --sourcemap",
"check-types": "tsc --noEmit",
"lint": "eslint .",
"test": "vitest",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export type { Position } from './position';
export type { Range } from './range';
export type { TextEdit } from './text-edit';
6 changes: 6 additions & 0 deletions packages/core/src/common/text-edit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Range } from './range';

export interface TextEdit {
range: Range;
newText: string;
}
7 changes: 6 additions & 1 deletion packages/core/src/diagnostic/diagnostic-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { DocumentManager } from '../document/document-manager';
import { Diagnostic } from './diagnostic';
import { DiagnosticFix } from './diagnostic-fix';

export type DiagnosticProviderFactory<T extends Document> = (DocumentManager: DocumentManager<T>) => DiagnosticProvider;
export type DiagnosticProviderFactory<T extends Document = Document> = (
DocumentManager: DocumentManager<T>,
) => DiagnosticProvider;
export type DiagnosticProviderConstructor<T extends Document = Document> = new (
documentManager: DocumentManager<T>,
) => DiagnosticProvider;

export interface DiagnosticsChanged {
uri: string;
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/document/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ export { ScriptureChapter } from './scripture-chapter';
export { ScriptureCharacterStyle } from './scripture-character-style';
export { ScriptureContainer } from './scripture-container';
export { ScriptureDocument } from './scripture-document';
export { ScriptureLeaf } from './scripture-leaf';
export { ScriptureMilestone } from './scripture-milestone';
export { ScriptureNode, ScriptureNodeType } from './scripture-node';
export type { ScriptureNode } from './scripture-node';
export { ScriptureNodeType } from './scripture-node';
export { ScriptureNote } from './scripture-note';
export { ScriptureOptBreak } from './scripture-optbreak';
export { ScriptureParagraph } from './scripture-paragraph';
Expand All @@ -19,3 +21,5 @@ export { ScriptureSidebar } from './scripture-sidebar';
export { ScriptureTable } from './scripture-table';
export { ScriptureText } from './scripture-text';
export { ScriptureVerse } from './scripture-verse';
export { TextDocument } from './text-document';
export { TextDocumentFactory } from './text-document-factory';
46 changes: 43 additions & 3 deletions packages/core/src/document/scripture-container.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,54 @@
import { Position } from '../common/position';
import { Range } from '../common/range';
import { ScriptureDocument } from './scripture-document';
import { ScriptureNode, ScriptureNodeType } from './scripture-node';

export abstract class ScriptureContainer extends ScriptureNode {
export abstract class ScriptureContainer implements ScriptureNode {
private _parent?: ScriptureNode;
private readonly _children: ScriptureNode[] = [];
readonly isLeaf = false;
range: Range = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };

constructor(children?: ScriptureNode[]) {
super();
if (children != null) {
for (const child of children) {
this.appendChild(child);
}
}
}

abstract readonly type: ScriptureNodeType;

get document(): ScriptureDocument | undefined {
return this._parent?.document;
}

get parent(): ScriptureNode | undefined {
return this._parent;
}

get children(): readonly ScriptureNode[] {
return this._children;
}

updateParent(parent: ScriptureNode | undefined): void {
this._parent = parent;
}

remove(): void {
if (this._parent == null) {
throw new Error('The node does not have a parent.');
}
this._parent.removeChild(this);
}

getText(): string {
if (this.document == null) {
throw new Error('The node is not part of a document.');
}
return this.document.getText(this.range);
}

*getNodes(filter?: ScriptureNodeType | ((node: ScriptureNode) => boolean)): IterableIterator<ScriptureNode> {
for (const child of this._children) {
if (filter == null || child.type === filter || (typeof filter === 'function' && filter(child))) {
Expand All @@ -25,6 +58,13 @@ export abstract class ScriptureContainer extends ScriptureNode {
}
}

positionAt(offset: number): Position {
if (this.document == null) {
throw new Error('The node is not part of a document.');
}
return this.document.positionAt(offset, this.range);
}

appendChild(child: ScriptureNode): void {
this._children.push(child);
child.updateParent(this);
Expand Down Expand Up @@ -57,7 +97,7 @@ export abstract class ScriptureContainer extends ScriptureNode {
}
}

clear(): void {
clearChildren(): void {
this._children.length = 0;
}
}
155 changes: 50 additions & 105 deletions packages/core/src/document/scripture-document.ts
Original file line number Diff line number Diff line change
@@ -1,139 +1,84 @@
import { Position } from '../common/position';
import { Range } from '../common/range';
import { Range } from '../common';
import { Document } from './document';
import { DocumentChange } from './document-factory';
import { ScriptureContainer } from './scripture-container';
import { ScriptureNode, ScriptureNodeType } from './scripture-node';
import { TextDocument } from './text-document';

export class ScriptureDocument extends ScriptureContainer implements Document {
private _lineOffsets: number[] | undefined = undefined;
private _content: string;
private _version: number;
export class ScriptureDocument extends TextDocument implements Document, ScriptureNode {
private readonly _children: ScriptureNode[] = [];
readonly parent: undefined = undefined;
readonly isLeaf = false;
readonly type = ScriptureNodeType.Document;
readonly document = this;
range: Range = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };

constructor(
public readonly uri: string,
version: number,
content: string,
children?: ScriptureNode[],
) {
super(children);
this._version = version;
this._content = content;
super(uri, version, content);
if (children != null) {
for (const child of children) {
this.appendChild(child);
}
}
}

get type(): ScriptureNodeType {
return ScriptureNodeType.Document;
get children(): readonly ScriptureNode[] {
return this._children;
}

get document(): this {
return this;
updateParent(_parent: ScriptureNode | undefined): void {
throw new Error('The method is not supported.');
}

get content(): string {
return this._content;
remove(): void {
throw new Error('The method is not supported.');
}

get version(): number {
return this._version;
*getNodes(filter?: ScriptureNodeType | ((node: ScriptureNode) => boolean)): IterableIterator<ScriptureNode> {
for (const child of this._children) {
if (filter == null || child.type === filter || (typeof filter === 'function' && filter(child))) {
yield child;
}
yield* child.getNodes(filter);
}
}

protected set version(value: number) {
this._version = value;
appendChild(child: ScriptureNode): void {
this._children.push(child);
child.updateParent(this);
}

getText(range?: Range): string {
if (range != null) {
const start = this.offsetAt(range.start);
const end = this.offsetAt(range.end);
return this._content.substring(start, end);
}
return this._content;
insertChild(index: number, child: ScriptureNode): void {
this._children.splice(index, 0, child);
child.updateParent(this);
}

offsetAt(position: Position): number {
const lineOffsets = this.getLineOffsets();
if (position.line >= lineOffsets.length) {
return this._content.length;
} else if (position.line < 0) {
return 0;
removeChild(child: ScriptureNode): void {
if (child.parent !== this) {
throw new Error('This node does not contain the specified child.');
}
const lineOffset = lineOffsets[position.line];
if (position.character <= 0) {
return lineOffset;
const index = this._children.indexOf(child);
if (index === -1) {
throw new Error('This node does not contain the specified child.');
}

const nextLineOffset =
position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : this._content.length;
const offset = Math.min(lineOffset + position.character, nextLineOffset);
return this.ensureBeforeEndOfLine(offset, lineOffset);
this._children.splice(index, 1);
child.updateParent(undefined);
}

protected updateContent(change: DocumentChange): void {
if (change.range == null) {
this._content = change.text;
this._lineOffsets = undefined;
} else {
const range = change.range;
const startOffset = this.offsetAt(range.start);
const endOffset = this.offsetAt(range.end);
this._content =
this._content.substring(0, startOffset) +
change.text +
this._content.substring(endOffset, this._content.length);

// update the offsets
const startLine = Math.max(range.start.line, 0);
const endLine = Math.max(range.end.line, 0);
let lineOffsets = this._lineOffsets!;
const addedLineOffsets = computeLineOffsets(change.text, false, startOffset);
if (endLine - startLine === addedLineOffsets.length) {
for (let i = 0, len = addedLineOffsets.length; i < len; i++) {
lineOffsets[i + startLine + 1] = addedLineOffsets[i];
}
} else {
if (addedLineOffsets.length < 10000) {
lineOffsets.splice(startLine + 1, endLine - startLine, ...addedLineOffsets);
} else {
// avoid too many arguments for splice
this._lineOffsets = lineOffsets = lineOffsets
.slice(0, startLine + 1)
.concat(addedLineOffsets, lineOffsets.slice(endLine + 1));
}
}
const diff = change.text.length - (endOffset - startOffset);
if (diff !== 0) {
for (let i = startLine + 1 + addedLineOffsets.length, len = lineOffsets.length; i < len; i++) {
lineOffsets[i] = lineOffsets[i] + diff;
}
}
spliceChildren(start: number, deleteCount: number, ...items: ScriptureNode[]): void {
const removed = this._children.splice(start, deleteCount, ...items);
for (const child of removed) {
child.updateParent(undefined);
}
}

public getLineOffsets(): number[] {
if (this._lineOffsets === undefined) {
this._lineOffsets = computeLineOffsets(this._content, true);
}
return this._lineOffsets;
}

public ensureBeforeEndOfLine(offset: number, lineOffset: number): number {
while (offset > lineOffset && (this._content[offset - 1] === '\r' || this._content[offset - 1] === '\n')) {
offset--;
for (const child of items) {
child.updateParent(this);
}
return offset;
}
}

function computeLineOffsets(text: string, isAtLineStart: boolean, textOffset = 0): number[] {
const result: number[] = isAtLineStart ? [textOffset] : [];
for (let i = 0; i < text.length; i++) {
const ch = text[i];
if (ch === '\n' || ch === '\r') {
if (ch === '\r' && i + 1 < text.length && text[i + 1] === '\n') {
i++;
}
result.push(textOffset + i + 1);
}
clearChildren(): void {
this._children.length = 0;
}
return result;
}
Loading

0 comments on commit 2f63b93

Please sign in to comment.