Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve lock code feature #121

Merged
merged 1 commit into from
Oct 9, 2024
Merged
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
120 changes: 73 additions & 47 deletions src/tools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as monaco from 'monaco-editor'
import { DisposableStore } from 'vscode/monaco'
import { IIdentifiedSingleEditOperation, ValidAnnotatedEditOperation } from 'vscode/vscode/vs/editor/common/model'

interface PastePayload {
text: string
Expand All @@ -12,11 +13,38 @@ function isPasteAction (handlerId: string, payload: unknown): payload is PastePa
return handlerId === 'paste'
}

export interface LockCodeOptions {
/**
* Error message displayed in a tooltip when an edit failed
*/
errorMessage?: string
/**
* Allows edit coming from a specific source
*/
allowChangeFromSources: string[]
/**
* Only take some decorations into account
*/
decorationFilter: (decoration: monaco.editor.IModelDecoration) => boolean
/**
* if true: when an edit block comes, either all the edit are applied or none
*/
transactionMode?: boolean
/**
* Should undo/redo be ignored
*/
allowUndoRedo?: boolean
}

export function lockCodeWithoutDecoration (
editor: monaco.editor.ICodeEditor,
decorationFilter: (decoration: monaco.editor.IModelDecoration) => boolean,
allowChangeFromSources: string[] = [],
errorMessage?: string
{
errorMessage,
allowChangeFromSources = [],
decorationFilter = () => true,
transactionMode = true,
allowUndoRedo = true
}: LockCodeOptions
): monaco.IDisposable {
const disposableStore = new DisposableStore()
function displayLockedCodeError (position: monaco.Position) {
Expand All @@ -43,19 +71,6 @@ export function lockCodeWithoutDecoration (
return false
}

const originalExecuteCommands = editor.executeCommands
editor.executeCommands = function (name, commands) {
for (const command of commands) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const range: monaco.Range | undefined = (command as any)?._range
if (range != null && !canEditRange(range)) {
displayLockedCodeError(range.getEndPosition())
return
}
}
return originalExecuteCommands.call(editor, name, commands)
}

const originalTrigger = editor.trigger
editor.trigger = function (source, handlerId, payload) {
// Try to transform whole file pasting into a paste in the editable area only
Expand Down Expand Up @@ -94,13 +109,6 @@ export function lockCodeWithoutDecoration (
}
}

if (['type', 'paste', 'cut'].includes(handlerId)) {
const selections = editor.getSelections()
if (selections != null && selections.some((range) => !canEditRange(range))) {
displayLockedCodeError(editor.getPosition()!)
return
}
}
return originalTrigger.call(editor, source, handlerId, payload)
}

Expand All @@ -115,36 +123,55 @@ export function lockCodeWithoutDecoration (
}
}

let restoreModelApplyEdit: () => void = () => {}
interface AugmentedITextModel extends monaco.editor.ITextModel {
_validateEditOperations(rawOperations: readonly IIdentifiedSingleEditOperation[]): ValidAnnotatedEditOperation[]
_isUndoing: boolean
_isRedoing: boolean
}

let restoreModel: (() => void) | undefined
function lockModel () {
restoreModelApplyEdit()
const model = editor.getModel()
restoreModel?.()
const model = editor.getModel() as AugmentedITextModel | undefined

if (model == null) {
return
}
const originalApplyEdit: (
operations: monaco.editor.IIdentifiedSingleEditOperation[],
computeUndoEdits?: boolean
) => void = model.applyEdits
model.applyEdits = ((
operations: monaco.editor.IIdentifiedSingleEditOperation[],
computeUndoEdits?: boolean
) => {

const original = model._validateEditOperations
model._validateEditOperations = function (this: AugmentedITextModel, rawOperations) {
const editorOperations: ValidAnnotatedEditOperation[] = original.call(this, rawOperations)

if (currentEditSource != null && allowChangeFromSources.includes(currentEditSource)) {
return originalApplyEdit.call(model, operations, computeUndoEdits!)
return editorOperations
}
const filteredOperations = operations.filter((operation) => canEditRange(operation.range))
if (filteredOperations.length === 0 && operations.length > 0) {
const firstRange = operations[0]!.range
displayLockedCodeError(
new monaco.Position(firstRange.startLineNumber, firstRange.startColumn)
)

if (allowUndoRedo && (this._isUndoing || this._isRedoing)) {
return editorOperations
}
return originalApplyEdit.call(model, filteredOperations, computeUndoEdits!)
}) as typeof model.applyEdits

restoreModelApplyEdit = () => {
model.applyEdits = originalApplyEdit as typeof model.applyEdits
if (transactionMode) {
const firstForbiddenOperation = editorOperations.find(operation => !canEditRange(operation.range))
if (firstForbiddenOperation != null) {
displayLockedCodeError(
new monaco.Position(firstForbiddenOperation.range.startLineNumber, firstForbiddenOperation.range.startColumn))
return []
} else {
return editorOperations
}
} else {
return editorOperations.filter(operation => {
if (!canEditRange(operation.range)) {
displayLockedCodeError(
new monaco.Position(operation.range.startLineNumber, operation.range.startColumn))
return false
}
return true
})
}
}
restoreModel = () => {
model._validateEditOperations = original
}
}
disposableStore.add(editor.onDidChangeModel(lockModel))
Expand Down Expand Up @@ -172,9 +199,8 @@ export function lockCodeWithoutDecoration (

disposableStore.add({
dispose () {
restoreModelApplyEdit()
restoreModel?.()
editor.executeEdits = originalExecuteEdit
editor.executeCommands = originalExecuteCommands
editor.trigger = originalTrigger
}
})
Expand Down
Loading