Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/core/src/lib/extmarks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2357,6 +2357,24 @@ Press ESC to return to main menu.`
const type60Marks = extmarks.getAllForTypeId(60)
expect(type60Marks.length).toBe(1)
})

it("should move an extmark to the updated typeId", async () => {
await setup("Hello World")

const id = extmarks.create({
start: 0,
end: 5,
typeId: 10,
})

expect(extmarks.update(id, { typeId: 20 })).toBe(true)
expect(extmarks.getAllForTypeId(10)).toHaveLength(0)

const marks = extmarks.getAllForTypeId(20)
expect(marks).toHaveLength(1)
expect(marks[0]?.id).toBe(id)
expect(extmarks.get(id)?.typeId).toBe(20)
})
})

describe("Undo/Redo with Extmarks", () => {
Expand Down
71 changes: 71 additions & 0 deletions packages/core/src/lib/extmarks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { EditBuffer } from "../edit-buffer.js"
import type { EditorView } from "../editor-view.js"
import type { MouseEvent } from "../renderer.js"
import { ExtmarksHistory, type ExtmarksSnapshot } from "./extmarks-history.js"
import type { MouseEventType } from "./parse.mouse.js"

export interface Extmark {
id: number
Expand All @@ -11,6 +13,9 @@ export interface Extmark {
priority?: number
data?: any
typeId: number
onClick?: (extmarkId: number, event: MouseEvent) => void
onMouseDown?: (extmarkId: number, event: MouseEvent) => void
onMouseUp?: (extmarkId: number, event: MouseEvent) => void
}

export interface ExtmarkOptions {
Expand All @@ -22,6 +27,9 @@ export interface ExtmarkOptions {
data?: any
typeId?: number
metadata?: any
onClick?: (extmarkId: number, event: MouseEvent) => void
onMouseDown?: (extmarkId: number, event: MouseEvent) => void
onMouseUp?: (extmarkId: number, event: MouseEvent) => void
}

/**
Expand Down Expand Up @@ -651,6 +659,9 @@ export class ExtmarksController {
priority: options.priority,
data: options.data,
typeId,
onClick: options.onClick,
onMouseDown: options.onMouseDown,
onMouseUp: options.onMouseUp,
}

this.extmarks.set(id, extmark)
Expand Down Expand Up @@ -703,6 +714,40 @@ export class ExtmarksController {
return Array.from(this.extmarks.values()).filter((e) => offset >= e.start && offset < e.end)
}

public update(id: number, options: Partial<ExtmarkOptions>): boolean {
if (this.destroyed) {
throw new Error("ExtmarksController is destroyed")
}

const extmark = this.extmarks.get(id)
if (!extmark) return false

if (options.start !== undefined) extmark.start = options.start
if (options.end !== undefined) extmark.end = options.end
if (options.virtual !== undefined) extmark.virtual = options.virtual
if (options.styleId !== undefined) extmark.styleId = options.styleId
if (options.priority !== undefined) extmark.priority = options.priority
if (options.data !== undefined) extmark.data = options.data
if (options.typeId !== undefined && options.typeId !== extmark.typeId) {
this.extmarksByTypeId.get(extmark.typeId)?.delete(id)
extmark.typeId = options.typeId
if (!this.extmarksByTypeId.has(extmark.typeId)) {
this.extmarksByTypeId.set(extmark.typeId, new Set())
}
this.extmarksByTypeId.get(extmark.typeId)!.add(id)
}
if (options.onClick !== undefined) extmark.onClick = options.onClick
if (options.onMouseDown !== undefined) extmark.onMouseDown = options.onMouseDown
if (options.onMouseUp !== undefined) extmark.onMouseUp = options.onMouseUp

if (options.metadata !== undefined) {
this.metadata.set(id, options.metadata)
}

this.updateHighlights()
return true
}

public getAllForTypeId(typeId: number): Extmark[] {
if (this.destroyed) return []
const ids = this.extmarksByTypeId.get(typeId)
Expand Down Expand Up @@ -806,6 +851,32 @@ export class ExtmarksController {
return this.metadata.get(extmarkId)
}

public handleMouseEvent(offset: number, event: MouseEvent): void {
if (this.destroyed) return

const extmarks = this.getAtOffset(offset)
for (const extmark of extmarks) {
const handler = this.getEventHandler(extmark, event.type)
if (handler) {
handler(extmark.id, event)
}
}
}

private getEventHandler(
extmark: Extmark,
eventType: MouseEventType,
): ((extmarkId: number, event: MouseEvent) => void) | undefined {
switch (eventType) {
case "down":
return extmark.onMouseDown
case "up":
return extmark.onMouseUp ?? extmark.onClick
default:
return undefined
}
}

public destroy(): void {
if (this.destroyed) return

Expand Down
27 changes: 25 additions & 2 deletions packages/core/src/renderables/EditBufferRenderable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { RGBA, parseColor } from "../lib/RGBA.js"
import type { RenderContext, Highlight, CursorStyleOptions, LineInfoProvider, LineInfo } from "../types.js"
import type { OptimizedBuffer } from "../buffer.js"
import { MeasureMode } from "yoga-layout"
import type { MouseEvent } from "../renderer.js"
import type { SyntaxStyle } from "../syntax-style.js"

export interface CursorChangeEvent {
Expand Down Expand Up @@ -62,6 +63,7 @@ export abstract class EditBufferRenderable extends Renderable implements LineInf
private _autoScrollAccumulator: number = 0
private _scrollSpeed: number = 16
private _keyboardSelectionActive: boolean = false
private _mouseOffset: number | undefined

public readonly editBuffer: EditBuffer
public readonly editorView: EditorView
Expand Down Expand Up @@ -352,13 +354,34 @@ export abstract class EditBufferRenderable extends Renderable implements LineInf
this._scrollSpeed = Math.max(0, value)
}

protected override onMouseEvent(event: any): void {
protected override onMouseEvent(event: MouseEvent): void {
if (event.type === "scroll") {
this.handleScroll(event)
return
}

if (event.type === "down") {
this._mouseOffset = this.cursorOffset
}

if (event.isDragging) {
if (event.type === "up") {
this._mouseOffset = undefined
}
return
}

if (event.type !== "down" && event.type !== "up") return
if (!this.editorView.extmarks) return

this.editorView.extmarks.handleMouseEvent(this._mouseOffset ?? this.cursorOffset, event)

if (event.type === "up") {
this._mouseOffset = undefined
}
}

protected handleScroll(event: any): void {
protected handleScroll(event: MouseEvent): void {
if (!event.scroll) return

const { direction, delta } = event.scroll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,34 @@ describe("Textarea - Selection Tests", () => {
expect(editor.getSelectedText()).toBe("World")
})

it("should not fire extmark click handlers during drag selection", async () => {
const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {
initialValue: "Hello World",
width: 40,
height: 10,
selectable: true,
})

let count = 0
editor.extmarks.create({
start: 0,
end: 5,
onClick: () => {
count += 1
},
onMouseUp: () => {
count += 1
},
})

await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)
await renderOnce()

expect(editor.hasSelection()).toBe(true)
expect(editor.getSelectedText()).toBe("Hello")
expect(count).toBe(0)
})

it("should render selection properly when drawing to buffer", async () => {
const buffer = OptimizedBuffer.create(80, 24, "wcwidth")

Expand Down