Skip to content

Commit

Permalink
refactor: move range and operations functions in separate files
Browse files Browse the repository at this point in the history
  • Loading branch information
KaiSaba committed Dec 9, 2024
1 parent b598071 commit 8faaeb6
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 175 deletions.
177 changes: 2 additions & 175 deletions src/tools.ts
Original file line number Diff line number Diff line change
@@ -1,181 +1,8 @@
import * as monaco from 'monaco-editor'
import { DisposableStore } from 'vscode/monaco'
import { IIdentifiedSingleEditOperation, ValidAnnotatedEditOperation } from 'vscode/vscode/vs/editor/common/model'

function getRangesFromDecorations (
editor: monaco.editor.ICodeEditor,
decorationFilter: (decoration: monaco.editor.IModelDecoration) => boolean
): monaco.Range[] {
const model = editor.getModel()
if (model == null) {
return []
}

return model
.getAllDecorations()
.filter(decorationFilter)
.map((decoration) => decoration.range)
}

function minusRanges (uniqueRange: monaco.Range, ranges: monaco.Range[]): monaco.Range[] {
const newRanges: monaco.Range[] = []
let lastEndPosition = uniqueRange.getStartPosition()

for (const range of ranges) {
const newRange = monaco.Range.fromPositions(lastEndPosition, range.getStartPosition())
lastEndPosition = range.getEndPosition()
newRanges.push(newRange)
}

if (lastEndPosition.isBefore(uniqueRange.getEndPosition())) {
newRanges.push(monaco.Range.fromPositions(lastEndPosition, uniqueRange.getEndPosition()))
}

return newRanges
}

function createNewOperation (
oldOperation: ValidAnnotatedEditOperation,
newRange: monaco.Range,
newText: string,
index: number
): ValidAnnotatedEditOperation {
const identifier = oldOperation.identifier != null
? { major: oldOperation.identifier.major, minor: oldOperation.identifier.minor + index }
: null
return new ValidAnnotatedEditOperation(
identifier,
newRange,
newText,
oldOperation.forceMoveMarkers,
oldOperation.isAutoWhitespaceEdit,
oldOperation._isTracked
)
}

function getLockedRanges (
editor: monaco.editor.ICodeEditor,
decorationFilter: (decoration: monaco.editor.IModelDecoration) => boolean,
withDecoration: boolean
): monaco.Range[] {
const model = editor.getModel()
if (model == null) {
return []
}

const fullModelRange = model.getFullModelRange()
const ranges = getRangesFromDecorations(editor, decorationFilter)
return withDecoration ? ranges : minusRanges(fullModelRange, ranges)
}

function getLockedRangeValueIndexesInText (
editor: monaco.editor.ICodeEditor,
range: monaco.Range,
text: string
): {
startIndex: number | null
endIndex: number | null
} {
const model = editor.getModel()
if (model == null) {
return { startIndex: null, endIndex: null }
}

const editorValue = editor.getValue()
const rangeValue = editorValue.slice(model.getOffsetAt(range.getStartPosition()), model.getOffsetAt(range.getEndPosition()))
const startIndex = text.indexOf(rangeValue)
return {
startIndex,
endIndex: startIndex + rangeValue.length
}
}

function computeNewOperationsWithIntersectingLockedCode (
editor: monaco.editor.ICodeEditor,
operation: ValidAnnotatedEditOperation,
uneditableRanges: monaco.Range[]
): ValidAnnotatedEditOperation[] {
const newOperations: ValidAnnotatedEditOperation[] = []
const editableRanges: monaco.Range[] = minusRanges(operation.range, uneditableRanges)

// Index of the current uneditable range in the text
let uneditableRangeIndex: number = 0
// Index of the current editable range in the text
let editableRangeIndex: number = 0
// The operation text is null or an empty string when it's a delete
let remainingText: string = operation.text ?? ''

do {
const editableRange = editableRanges[editableRangeIndex]
if (editableRange == null) {
// There are no editable ranges left
return newOperations
}

const uneditableRange = uneditableRanges[uneditableRangeIndex]
if (uneditableRange == null) {
// There are no more locked ranges
return [
...newOperations,
createNewOperation(operation, editableRange, remainingText, editableRangeIndex)
]
}

const { startIndex, endIndex } = getLockedRangeValueIndexesInText(editor, uneditableRange, remainingText)
if (startIndex == null || endIndex == null) {
return newOperations
} else if (startIndex === -1) {
// The uneditable text is not in the remaining operation text
return [
...newOperations,
createNewOperation(operation, editableRange, remainingText, editableRangeIndex)
]
// remainingText = null
} else if (startIndex === 0) {
// The uneditable text is at the beginning of the remaining operation text
uneditableRangeIndex++
remainingText = remainingText.slice(endIndex)
} else {
// The uneditable text is in the middle or at the end of the remaining operation text
newOperations.push(
createNewOperation(operation, editableRange, remainingText.slice(0, startIndex), editableRangeIndex)
)
uneditableRangeIndex++
editableRangeIndex++
remainingText = remainingText.slice(endIndex)
}
} while (remainingText.length > 0)

return newOperations
}

function computeNewOperationsForLockedCode (
editor: monaco.editor.ICodeEditor,
decorationFilter: (decoration: monaco.editor.IModelDecoration) => boolean,
editorOperations: ValidAnnotatedEditOperation[],
withDecoration: boolean
): ValidAnnotatedEditOperation[] {
const uneditableRanges = getLockedRanges(editor, decorationFilter, withDecoration)
if (uneditableRanges.length <= 0) {
return editorOperations
}

const newOperations: ValidAnnotatedEditOperation[] = []
for (const operation of editorOperations) {
const operationRange = operation.range
const uneditableRangesThatIntersects = uneditableRanges.filter(range => monaco.Range.areIntersecting(range, operationRange))

if (uneditableRangesThatIntersects.length <= 0) {
// The operation range doesn't intersect with an uneditable range
newOperations.push(operation)
} else {
// The operation range intersects with one or more uneditable range
newOperations.push(...computeNewOperationsWithIntersectingLockedCode(editor, operation, uneditableRangesThatIntersects))
}
}

return newOperations
}
import { getRangesFromDecorations } from './tools/utils/rangeUtils'
import { computeNewOperationsForLockedCode } from './tools/utils/editorOperationUtils'

/**
* Exctract ranges between startToken and endToken
Expand Down
130 changes: 130 additions & 0 deletions src/tools/utils/editorOperationUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import * as monaco from 'monaco-editor'
import { ValidAnnotatedEditOperation } from 'vscode/vscode/vs/editor/common/model'
import { getLockedRanges, minusRanges } from './rangeUtils'

function createNewOperation (
oldOperation: ValidAnnotatedEditOperation,
newRange: monaco.Range,
newText: string | null,
index: number
): ValidAnnotatedEditOperation {
const identifier = oldOperation.identifier != null
? { major: oldOperation.identifier.major, minor: oldOperation.identifier.minor + index }
: null
return new ValidAnnotatedEditOperation(
identifier,
newRange,
newText,
oldOperation.forceMoveMarkers,
oldOperation.isAutoWhitespaceEdit,
oldOperation._isTracked
)
}

function getLockedRangeValueIndexesInText (
editor: monaco.editor.ICodeEditor,
range: monaco.Range,
text: string
): {
startIndex: number | null
endIndex: number | null
} {
const model = editor.getModel()
if (model == null) {
return { startIndex: null, endIndex: null }
}

const rangeValue = model.getValueInRange(range)
const startIndex = text.indexOf(rangeValue)
return {
startIndex,
endIndex: startIndex + rangeValue.length
}
}

function computeNewOperationsWithIntersectingLockedCode (
editor: monaco.editor.ICodeEditor,
operation: ValidAnnotatedEditOperation,
uneditableRanges: monaco.Range[]
): ValidAnnotatedEditOperation[] {
const newOperations: ValidAnnotatedEditOperation[] = []
const editableRanges: monaco.Range[] = minusRanges(operation.range, uneditableRanges)

// Index of the current uneditable range in the text
let uneditableRangeIndex: number = 0
// Index of the current editable range in the text
let editableRangeIndex: number = 0
// The operation text is null or an empty string when it's a delete
let remainingText: string = operation.text ?? ''

do {
const editableRange = editableRanges[editableRangeIndex]
if (editableRange == null) {
// There are no editable ranges left
return newOperations
}

const uneditableRange = uneditableRanges[uneditableRangeIndex]
if (uneditableRange == null) {
// There are no more locked ranges
return [
...newOperations,
createNewOperation(operation, editableRange, remainingText, editableRangeIndex)
]
}

const { startIndex, endIndex } = getLockedRangeValueIndexesInText(editor, uneditableRange, remainingText)
if (startIndex == null || endIndex == null) {
return newOperations
} else if (startIndex === -1) {
// The uneditable text is not in the remaining operation text
return [
...newOperations,
createNewOperation(operation, editableRange, remainingText, editableRangeIndex)
]
// remainingText = null
} else if (startIndex === 0) {
// The uneditable text is at the beginning of the remaining operation text
uneditableRangeIndex++
remainingText = remainingText.slice(endIndex)
} else {
// The uneditable text is in the middle or at the end of the remaining operation text
newOperations.push(
createNewOperation(operation, editableRange, remainingText.slice(0, startIndex), editableRangeIndex)
)
uneditableRangeIndex++
editableRangeIndex++
remainingText = remainingText.slice(endIndex)
}
} while (remainingText.length > 0)

return newOperations
}

export function computeNewOperationsForLockedCode (
editor: monaco.editor.ICodeEditor,
decorationFilter: (decoration: monaco.editor.IModelDecoration) => boolean,
editorOperations: ValidAnnotatedEditOperation[],
withDecoration: boolean
): ValidAnnotatedEditOperation[] {
const uneditableRanges = getLockedRanges(editor, decorationFilter, withDecoration)
if (uneditableRanges.length <= 0) {
return editorOperations
}

const newOperations: ValidAnnotatedEditOperation[] = []
for (const operation of editorOperations) {
const operationRange = operation.range
const uneditableRangesThatIntersects = uneditableRanges.filter(range => monaco.Range.areIntersecting(range, operationRange))

if (uneditableRangesThatIntersects.length <= 0) {
// The operation range doesn't intersect with an uneditable range
newOperations.push(operation)
} else {
// The operation range intersects with one or more uneditable range
newOperations.push(...computeNewOperationsWithIntersectingLockedCode(editor, operation, uneditableRangesThatIntersects))
}
}

return newOperations
}
49 changes: 49 additions & 0 deletions src/tools/utils/rangeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as monaco from 'monaco-editor'

export function getRangesFromDecorations (
editor: monaco.editor.ICodeEditor,
decorationFilter: (decoration: monaco.editor.IModelDecoration) => boolean
): monaco.Range[] {
const model = editor.getModel()
if (model == null) {
return []
}

return model
.getAllDecorations()
.filter(decorationFilter)
.map((decoration) => decoration.range)
}

export function minusRanges (uniqueRange: monaco.Range, ranges: monaco.Range[]): monaco.Range[] {
const newRanges: monaco.Range[] = []
let lastEndPosition = uniqueRange.getStartPosition()
const intersectingRanges = ranges.filter(range => monaco.Range.areIntersecting(range, uniqueRange))

for (const range of intersectingRanges) {
const newRange = monaco.Range.fromPositions(lastEndPosition, range.getStartPosition())
lastEndPosition = range.getEndPosition()
newRanges.push(newRange)
}

if (lastEndPosition.isBeforeOrEqual(uniqueRange.getEndPosition())) {
newRanges.push(monaco.Range.fromPositions(lastEndPosition, uniqueRange.getEndPosition()))
}

return newRanges
}

export function getLockedRanges (
editor: monaco.editor.ICodeEditor,
decorationFilter: (decoration: monaco.editor.IModelDecoration) => boolean,
withDecoration: boolean
): monaco.Range[] {
const model = editor.getModel()
if (model == null) {
return []
}

const fullModelRange = model.getFullModelRange()
const ranges = getRangesFromDecorations(editor, decorationFilter)
return withDecoration ? ranges : minusRanges(fullModelRange, ranges)
}

0 comments on commit 8faaeb6

Please sign in to comment.