Skip to content

Commit 5cd633e

Browse files
author
Loïc Mangeonjean
committed
feat: add more tools
1 parent 3d540f4 commit 5cd633e

File tree

1 file changed

+275
-0
lines changed

1 file changed

+275
-0
lines changed

src/tools.ts

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as monaco from 'monaco-editor'
2+
import { ContextKeyExpr, DisposableStore, KeybindingsRegistry } from 'vscode/monaco'
23

34
interface PastePayload {
45
text: string
@@ -316,3 +317,277 @@ export async function collapseCodeSections (editor: monaco.editor.ICodeEditor, s
316317
}
317318
}
318319
}
320+
321+
interface IDecorationProvider {
322+
provideDecorations (model: monaco.editor.ITextModel): monaco.editor.IModelDeltaDecoration[]
323+
}
324+
325+
export function registerTextDecorationProvider (provider: IDecorationProvider): monaco.IDisposable {
326+
const disposableStore = new DisposableStore()
327+
328+
const watchEditor = (editor: monaco.editor.ICodeEditor): monaco.IDisposable => {
329+
const disposableStore = new DisposableStore()
330+
const decorationCollection = editor.createDecorationsCollection()
331+
332+
const checkEditor = () => {
333+
const model = editor.getModel()
334+
if (model != null) {
335+
decorationCollection.set(provider.provideDecorations(model))
336+
} else {
337+
decorationCollection.clear()
338+
}
339+
}
340+
341+
disposableStore.add(editor.onDidChangeModel(checkEditor))
342+
disposableStore.add(editor.onDidChangeModelContent(checkEditor))
343+
disposableStore.add({
344+
dispose () {
345+
decorationCollection.clear()
346+
}
347+
})
348+
checkEditor()
349+
return disposableStore
350+
}
351+
352+
monaco.editor.getEditors().forEach(editor => disposableStore.add(watchEditor(editor)))
353+
disposableStore.add(monaco.editor.onDidCreateEditor(editor => disposableStore.add(watchEditor(editor))))
354+
355+
return disposableStore
356+
}
357+
358+
export function runOnAllEditors (cb: (editor: monaco.editor.ICodeEditor) => monaco.IDisposable): monaco.IDisposable {
359+
const disposableStore = new DisposableStore()
360+
361+
const handleEditor = (editor: monaco.editor.ICodeEditor) => {
362+
const disposable = cb(editor)
363+
disposableStore.add(disposable)
364+
const disposeEventDisposable = editor.onDidDispose(() => {
365+
disposableStore.delete(disposable)
366+
disposableStore.delete(disposeEventDisposable)
367+
})
368+
disposableStore.add(disposeEventDisposable)
369+
}
370+
monaco.editor.getEditors().forEach(handleEditor)
371+
disposableStore.add(monaco.editor.onDidCreateEditor(handleEditor))
372+
373+
return disposableStore
374+
}
375+
376+
export function preventAlwaysConsumeTouchEvent (editor: monaco.editor.ICodeEditor): void {
377+
let firstX = 0
378+
let firstY = 0
379+
let atTop = false
380+
let atBottom = false
381+
let atLeft = false
382+
let atRight = false
383+
let useBrowserBehavior: null | boolean = null
384+
385+
editor.onDidChangeModel(() => {
386+
const domNode = editor.getDomNode()
387+
if (domNode == null) {
388+
return
389+
}
390+
domNode.addEventListener('touchstart', (e) => {
391+
const firstTouch = e.targetTouches.item(0)
392+
if (firstTouch == null) {
393+
return
394+
}
395+
396+
// Prevent monaco-editor from trying to call preventDefault on the touchstart event
397+
// so we'll be able to use the default behavior of the touchmove event
398+
e.preventDefault = () => {}
399+
400+
firstX = firstTouch.clientX
401+
firstY = firstTouch.clientY
402+
403+
const layoutInfo = editor.getLayoutInfo()
404+
atTop = editor.getScrollTop() <= 0
405+
atBottom = editor.getScrollTop() >= editor.getContentHeight() - layoutInfo.height
406+
atLeft = editor.getScrollLeft() <= 0
407+
atRight = editor.getScrollLeft() >= editor.getContentWidth() - layoutInfo.width
408+
useBrowserBehavior = null
409+
})
410+
domNode.addEventListener('touchmove', (e) => {
411+
const firstTouch = e.changedTouches.item(0)
412+
if (firstTouch == null) {
413+
return
414+
}
415+
416+
if (useBrowserBehavior == null) {
417+
const dx = firstTouch.clientX - firstX
418+
const dy = firstTouch.clientY - firstY
419+
if (Math.abs(dx) > Math.abs(dy)) {
420+
// It's an horizontal scroll
421+
useBrowserBehavior = (dx < 0 && atRight) || (dx > 0 && atLeft)
422+
} else {
423+
// It's a vertical scroll
424+
useBrowserBehavior = (dy < 0 && atBottom) || (dy > 0 && atTop)
425+
}
426+
}
427+
if (useBrowserBehavior) {
428+
// Stop the event before monaco tries to preventDefault on it
429+
e.stopPropagation()
430+
}
431+
})
432+
domNode.addEventListener('touchend', (e) => {
433+
if (useBrowserBehavior ?? false) {
434+
// Prevent monaco from trying to open its context menu
435+
// It thinks it's a long press because it didn't receive the move events
436+
e.stopPropagation()
437+
}
438+
})
439+
})
440+
}
441+
442+
// https://github.com/microsoft/monaco-editor/issues/568
443+
class PlaceholderContentWidget implements monaco.editor.IContentWidget {
444+
private static readonly ID = 'editor.widget.placeholderHint'
445+
446+
private domNode: HTMLElement | undefined
447+
448+
constructor (
449+
private readonly editor: monaco.editor.ICodeEditor,
450+
private readonly placeholder: string
451+
) {}
452+
453+
getId (): string {
454+
return PlaceholderContentWidget.ID
455+
}
456+
457+
getDomNode (): HTMLElement {
458+
if (this.domNode == null) {
459+
this.domNode = document.createElement('pre')
460+
this.domNode.style.width = 'max-content'
461+
this.domNode.textContent = this.placeholder
462+
this.domNode.style.pointerEvents = 'none'
463+
this.domNode.style.color = '#aaa'
464+
this.domNode.style.margin = '0'
465+
466+
this.editor.applyFontInfo(this.domNode)
467+
}
468+
469+
return this.domNode
470+
}
471+
472+
getPosition (): monaco.editor.IContentWidgetPosition | null {
473+
return {
474+
position: { lineNumber: 1, column: 1 },
475+
preference: [monaco.editor.ContentWidgetPositionPreference.EXACT]
476+
}
477+
}
478+
}
479+
480+
export function addPlaceholder (
481+
editor: monaco.editor.ICodeEditor,
482+
placeholder: string
483+
): monaco.IDisposable {
484+
const widget = new PlaceholderContentWidget(editor, placeholder)
485+
486+
function onDidChangeModelContent (): void {
487+
if (editor.getValue() === '') {
488+
editor.addContentWidget(widget)
489+
} else {
490+
editor.removeContentWidget(widget)
491+
}
492+
}
493+
494+
onDidChangeModelContent()
495+
const changeDisposable = editor.onDidChangeModelContent(() => onDidChangeModelContent())
496+
return {
497+
dispose () {
498+
changeDisposable.dispose()
499+
editor.removeContentWidget(widget)
500+
}
501+
}
502+
}
503+
504+
export function mapClipboard (
505+
editor: monaco.editor.ICodeEditor,
506+
{
507+
toClipboard,
508+
fromClipboard
509+
}: {
510+
toClipboard: (data: string) => string
511+
fromClipboard: (data: string) => string
512+
}
513+
): monaco.IDisposable {
514+
const disposableStore = new DisposableStore()
515+
let copiedText = ''
516+
517+
disposableStore.add(
518+
KeybindingsRegistry.registerCommandAndKeybindingRule({
519+
id: 'customCopy',
520+
weight: 1000,
521+
handler: () => {
522+
copiedText = editor.getModel()!.getValueInRange(editor.getSelection()!)
523+
document.execCommand('copy')
524+
},
525+
when: ContextKeyExpr.equals('editorId', editor.getId()),
526+
primary: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyC
527+
})
528+
)
529+
530+
disposableStore.add(
531+
KeybindingsRegistry.registerCommandAndKeybindingRule({
532+
id: 'customCut',
533+
weight: 1000,
534+
handler: () => {
535+
copiedText = editor.getModel()!.getValueInRange(editor.getSelection()!)
536+
document.execCommand('copy')
537+
},
538+
when: ContextKeyExpr.equals('editorId', editor.getId()),
539+
primary: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyX
540+
})
541+
)
542+
543+
const originalTrigger = editor.trigger
544+
editor.trigger = function (source, handlerId, payload) {
545+
if (handlerId === 'editor.action.clipboardCopyAction') {
546+
copiedText = editor.getModel()!.getValueInRange(editor.getSelection()!)
547+
} else if (handlerId === 'editor.action.clipboardCutAction') {
548+
copiedText = editor.getModel()!.getValueInRange(editor.getSelection()!)
549+
} else if (handlerId === 'paste') {
550+
const newText = fromClipboard(payload.text)
551+
if (newText !== payload.text) {
552+
payload = {
553+
...payload,
554+
text: newText
555+
}
556+
}
557+
}
558+
originalTrigger.call(this, source, handlerId, payload)
559+
}
560+
disposableStore.add({
561+
dispose () {
562+
editor.trigger = originalTrigger
563+
}
564+
})
565+
566+
function mapCopy (event: ClipboardEvent): void {
567+
const clipdata = event.clipboardData ?? (window as unknown as { clipboardData: DataTransfer }).clipboardData
568+
let content = clipdata.getData('Text')
569+
if (content.length === 0) {
570+
content = copiedText
571+
}
572+
const transformed = toClipboard(content)
573+
if (transformed !== content) {
574+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
575+
if (clipdata.types != null) {
576+
clipdata.types.forEach(type => clipdata.setData(type, toClipboard(content)))
577+
} else {
578+
clipdata.setData('text/plain', toClipboard(content))
579+
}
580+
}
581+
}
582+
const editorDomNode = editor.getContainerDomNode()
583+
editorDomNode.addEventListener('copy', mapCopy)
584+
editorDomNode.addEventListener('cut', mapCopy)
585+
disposableStore.add({
586+
dispose () {
587+
editorDomNode.removeEventListener('copy', mapCopy)
588+
editorDomNode.removeEventListener('cut', mapCopy)
589+
}
590+
})
591+
592+
return disposableStore
593+
}

0 commit comments

Comments
 (0)