Skill update round trip / roundtrip (false, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, latest) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
279 lines
8.8 KiB
TypeScript
279 lines
8.8 KiB
TypeScript
import {
|
|
richMarkdownContextMenuCommandChannel,
|
|
type RichMarkdownContextMenuCommand,
|
|
type RichMarkdownContextMenuCommandPayload,
|
|
type RichMarkdownContextMenuTableTarget
|
|
} from '../../shared/rich-markdown-context-menu'
|
|
import { translateMain } from '../i18n/main-i18n'
|
|
|
|
type EditableContextMenuWebContents = Pick<
|
|
Electron.WebContents,
|
|
'replaceMisspelling' | 'send' | 'session'
|
|
>
|
|
|
|
function markdownCommandItem(
|
|
label: string,
|
|
command: RichMarkdownContextMenuCommand,
|
|
webContents: EditableContextMenuWebContents,
|
|
point: { x: number; y: number },
|
|
tableTargetId?: string
|
|
): Electron.MenuItemConstructorOptions {
|
|
return {
|
|
label,
|
|
click: () => {
|
|
const payload: RichMarkdownContextMenuCommandPayload = {
|
|
command,
|
|
...point,
|
|
...(tableTargetId ? { tableTargetId } : {})
|
|
}
|
|
webContents.send(richMarkdownContextMenuCommandChannel, payload)
|
|
}
|
|
}
|
|
}
|
|
|
|
function editableContextPasteItem(
|
|
label: string,
|
|
webContents: EditableContextMenuWebContents,
|
|
options?: { plainTextOnly?: boolean }
|
|
): Electron.MenuItemConstructorOptions {
|
|
return {
|
|
label,
|
|
click: () => {
|
|
// Why: context-menu paste must share renderer ownership with keyboard and
|
|
// app-menu paste so large text controls can chunk and terminals cannot
|
|
// receive duplicate native paste.
|
|
webContents.send('ui:editableContextPaste', {
|
|
plainTextOnly: options?.plainTextOnly === true
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
function buildMarkdownMenuTemplate(
|
|
webContents: EditableContextMenuWebContents,
|
|
point: { x: number; y: number },
|
|
tableTarget: RichMarkdownContextMenuTableTarget | null
|
|
): Electron.MenuItemConstructorOptions[] {
|
|
return [
|
|
markdownCommandItem('Add link', 'add-link', webContents, point),
|
|
{ type: 'separator' },
|
|
{
|
|
label: 'Format',
|
|
submenu: [
|
|
markdownCommandItem('Bold', 'bold', webContents, point),
|
|
markdownCommandItem('Italic', 'italic', webContents, point),
|
|
markdownCommandItem('Strike', 'strike', webContents, point),
|
|
markdownCommandItem('Inline code', 'inline-code', webContents, point),
|
|
markdownCommandItem('Code block', 'code-block', webContents, point),
|
|
markdownCommandItem('Quote', 'blockquote', webContents, point)
|
|
]
|
|
},
|
|
{
|
|
label: 'Paragraph',
|
|
submenu: [
|
|
markdownCommandItem('Body text', 'paragraph', webContents, point),
|
|
markdownCommandItem('Heading 1', 'heading-1', webContents, point),
|
|
markdownCommandItem('Heading 2', 'heading-2', webContents, point),
|
|
markdownCommandItem('Heading 3', 'heading-3', webContents, point),
|
|
markdownCommandItem('Heading 4', 'heading-4', webContents, point),
|
|
markdownCommandItem('Heading 5', 'heading-5', webContents, point),
|
|
{ type: 'separator' },
|
|
markdownCommandItem('Bullet list', 'bullet-list', webContents, point),
|
|
markdownCommandItem('Numbered list', 'ordered-list', webContents, point),
|
|
markdownCommandItem('Checklist', 'task-list', webContents, point)
|
|
]
|
|
},
|
|
{
|
|
label: 'Insert',
|
|
submenu: [
|
|
markdownCommandItem('Link', 'add-link', webContents, point),
|
|
markdownCommandItem('Image', 'image', webContents, point),
|
|
markdownCommandItem('Divider', 'divider', webContents, point),
|
|
markdownCommandItem('Code block', 'code-block', webContents, point)
|
|
]
|
|
},
|
|
...(tableTarget
|
|
? [
|
|
{
|
|
label: translateMain('auto.main.window.editableContextMenu.table', 'Table'),
|
|
submenu: [
|
|
{
|
|
...markdownCommandItem(
|
|
translateMain(
|
|
'auto.main.window.editableContextMenu.insertRowAbove',
|
|
'Insert row above'
|
|
),
|
|
'insert-row-above',
|
|
webContents,
|
|
point,
|
|
tableTarget.targetId
|
|
),
|
|
enabled: tableTarget.cellType !== 'header'
|
|
},
|
|
markdownCommandItem(
|
|
translateMain(
|
|
'auto.main.window.editableContextMenu.insertRowBelow',
|
|
'Insert row below'
|
|
),
|
|
'insert-row-below',
|
|
webContents,
|
|
point,
|
|
tableTarget.targetId
|
|
),
|
|
{
|
|
...markdownCommandItem(
|
|
translateMain('auto.main.window.editableContextMenu.deleteRow', 'Delete row'),
|
|
'delete-row',
|
|
webContents,
|
|
point,
|
|
tableTarget.targetId
|
|
),
|
|
enabled: tableTarget.cellType !== 'header'
|
|
},
|
|
{ type: 'separator' as const },
|
|
markdownCommandItem(
|
|
translateMain(
|
|
'auto.main.window.editableContextMenu.insertColumnLeft',
|
|
'Insert column left'
|
|
),
|
|
'insert-column-left',
|
|
webContents,
|
|
point,
|
|
tableTarget.targetId
|
|
),
|
|
markdownCommandItem(
|
|
translateMain(
|
|
'auto.main.window.editableContextMenu.insertColumnRight',
|
|
'Insert column right'
|
|
),
|
|
'insert-column-right',
|
|
webContents,
|
|
point,
|
|
tableTarget.targetId
|
|
),
|
|
markdownCommandItem(
|
|
translateMain('auto.main.window.editableContextMenu.deleteColumn', 'Delete column'),
|
|
'delete-column',
|
|
webContents,
|
|
point,
|
|
tableTarget.targetId
|
|
),
|
|
{ type: 'separator' as const },
|
|
markdownCommandItem(
|
|
translateMain('auto.main.window.editableContextMenu.deleteTable', 'Delete table'),
|
|
'delete-table',
|
|
webContents,
|
|
point,
|
|
tableTarget.targetId
|
|
)
|
|
]
|
|
}
|
|
]
|
|
: []),
|
|
{ type: 'separator' },
|
|
{ role: 'cut' },
|
|
{ role: 'copy' },
|
|
editableContextPasteItem('Paste', webContents),
|
|
editableContextPasteItem('Paste as plain text', webContents, { plainTextOnly: true }),
|
|
{ role: 'selectAll' }
|
|
]
|
|
}
|
|
|
|
function buildNativeEditMenuTemplate(
|
|
webContents: EditableContextMenuWebContents
|
|
): Electron.MenuItemConstructorOptions[] {
|
|
return [
|
|
{ role: 'cut' },
|
|
{ role: 'copy' },
|
|
editableContextPasteItem('Paste', webContents),
|
|
editableContextPasteItem('Paste as plain text', webContents, { plainTextOnly: true }),
|
|
{ role: 'selectAll' }
|
|
]
|
|
}
|
|
|
|
export function buildEditableContextMenuTemplate(
|
|
params: Electron.ContextMenuParams,
|
|
webContents: EditableContextMenuWebContents,
|
|
options?: { tableTarget?: RichMarkdownContextMenuTableTarget | null }
|
|
): Electron.MenuItemConstructorOptions[] {
|
|
if (!params.isEditable) {
|
|
return []
|
|
}
|
|
|
|
const suggestions = params.dictionarySuggestions.slice(0, 5)
|
|
const isRichMarkdownSurface = params.formControlType === 'none'
|
|
const template: Electron.MenuItemConstructorOptions[] = suggestions.map((suggestion) => ({
|
|
label: suggestion,
|
|
click: () => webContents.replaceMisspelling(suggestion)
|
|
}))
|
|
|
|
if (params.misspelledWord) {
|
|
if (template.length > 0) {
|
|
template.push({ type: 'separator' })
|
|
}
|
|
template.push({
|
|
label: 'Add to dictionary',
|
|
click: () => {
|
|
webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
|
|
}
|
|
})
|
|
}
|
|
|
|
if (template.length > 0) {
|
|
template.push({ type: 'separator' })
|
|
}
|
|
template.push(
|
|
...(isRichMarkdownSurface
|
|
? buildMarkdownMenuTemplate(
|
|
webContents,
|
|
{ x: params.x, y: params.y },
|
|
options?.tableTarget ?? null
|
|
)
|
|
: buildNativeEditMenuTemplate(webContents))
|
|
)
|
|
|
|
return template
|
|
}
|
|
|
|
export function parseRichMarkdownContextMenuTableTarget(
|
|
value: unknown
|
|
): RichMarkdownContextMenuTableTarget | null {
|
|
if (!value || typeof value !== 'object') {
|
|
return null
|
|
}
|
|
const target = value as Partial<RichMarkdownContextMenuTableTarget>
|
|
if (
|
|
(target.cellType !== 'body' && target.cellType !== 'header') ||
|
|
typeof target.targetId !== 'string' ||
|
|
target.targetId.length === 0 ||
|
|
typeof target.x !== 'number' ||
|
|
!Number.isFinite(target.x) ||
|
|
typeof target.y !== 'number' ||
|
|
!Number.isFinite(target.y)
|
|
) {
|
|
return null
|
|
}
|
|
return {
|
|
cellType: target.cellType,
|
|
targetId: target.targetId,
|
|
x: target.x,
|
|
y: target.y
|
|
}
|
|
}
|
|
|
|
export function matchingRichMarkdownContextMenuTableTarget(
|
|
params: Electron.ContextMenuParams,
|
|
target: RichMarkdownContextMenuTableTarget | null
|
|
): RichMarkdownContextMenuTableTarget | null {
|
|
if (
|
|
!target ||
|
|
!params.isEditable ||
|
|
params.formControlType !== 'none' ||
|
|
params.x !== target.x ||
|
|
params.y !== target.y
|
|
) {
|
|
return null
|
|
}
|
|
return target
|
|
}
|