pull/2908/head
filip mertens 2 years ago
parent 6380739517
commit 60b93ce772
  1. 4
      apps/remix-ide/src/app/editor/editor.js
  2. 37
      apps/remix-ide/src/app/plugins/parser/code-parser.tsx
  3. 54
      apps/remix-ide/src/app/plugins/parser/services/antlr-worker.ts
  4. 154
      apps/remix-ide/src/app/plugins/parser/services/code-parser-antlr-service.ts
  5. 1952
      apps/remix-ide/src/assets/js/parser/antlr.js
  6. 6
      apps/remix-ide/src/assets/js/parser/antlr.js.map
  7. 42251
      apps/remix-ide/src/assets/js/parser/index.iife.js
  8. 7
      apps/remix-ide/src/assets/js/parser/index.iife.js.map
  9. 2
      apps/remix-ide/src/index.html
  10. 128
      libs/remix-ui/editor/src/lib/providers/completionProvider.ts
  11. 2
      libs/remix-ui/editor/src/lib/providers/definitionProvider.ts
  12. 6
      libs/remix-ui/editor/src/lib/remix-ui-editor.tsx

@ -367,8 +367,8 @@ class Editor extends Plugin {
/**
* The position of the cursor
*/
getCursorPosition () {
return this.api.getCursorPosition()
getCursorPosition (offset = true) {
return this.api.getCursorPosition(offset)
}
/**

@ -57,8 +57,6 @@ interface codeParserIndex {
export class CodeParser extends Plugin {
antlrParserResult: antlr.ParseResult // contains the simple parsed AST for the current file
compilerAbstract: CompilerAbstract
currentFile: string
nodeIndex: codeParserIndex
@ -90,14 +88,13 @@ export class CodeParser extends Plugin {
async handleChangeEvents() {
const completionSettings = await this.call('config', 'getAppParameter', 'auto-completion')
if (completionSettings) {
// current timestamp
console.log('get ast', Date.now())
//this.antlrService.getCurrentFileAST()
console.log('done', Date.now())
this.antlrService.enableWorker()
} else {
this.antlrService.disableWorker()
}
const showGasSettings = await this.call('config', 'getAppParameter', 'show-gas')
const showErrorSettings = await this.call('config', 'getAppParameter', 'display-errors')
if(showGasSettings || showErrorSettings || completionSettings) {
if (showGasSettings || showErrorSettings || completionSettings) {
await this.compilerService.compile()
}
}
@ -127,6 +124,10 @@ export class CodeParser extends Plugin {
this.on('fileManager', 'currentFileChanged', async () => {
await this.call('editor', 'discardLineTexts')
const completionSettings = await this.call('config', 'getAppParameter', 'auto-completion')
if (completionSettings) {
this.antlrService.getCurrentFileAST()
}
await this.handleChangeEvents()
})
@ -408,19 +409,21 @@ export class CodeParser extends Plugin {
return nodeDefinition
} else {
const astNodes = await this.antlrService.listAstNodes()
for (const node of astNodes) {
if (node.range[0] <= position && node.range[1] >= position) {
if (nodeDefinition && nodeDefinition.range[0] < node.range[0]) {
nodeDefinition = node
if (astNodes && astNodes.length) {
for (const node of astNodes) {
if (node.range[0] <= position && node.range[1] >= position) {
if (nodeDefinition && nodeDefinition.range[0] < node.range[0]) {
nodeDefinition = node
}
if (!nodeDefinition) nodeDefinition = node
}
if (!nodeDefinition) nodeDefinition = node
}
if (nodeDefinition && nodeDefinition.type && nodeDefinition.type === 'Identifier') {
const nodeForIdentifier = await this.findIdentifier(nodeDefinition)
if (nodeForIdentifier) nodeDefinition = nodeForIdentifier
}
return nodeDefinition
}
if (nodeDefinition && nodeDefinition.type && nodeDefinition.type === 'Identifier') {
const nodeForIdentifier = await this.findIdentifier(nodeDefinition)
if (nodeForIdentifier) nodeDefinition = nodeForIdentifier
}
return nodeDefinition
}
}

@ -0,0 +1,54 @@
let parser: any
// 'DedicatedWorkerGlobalScope' object (the Worker global scope) is accessible through the self keyword
// 'dom' and 'webworker' library files can't be included together https://github.com/microsoft/TypeScript/issues/20595
export default function (self) { // eslint-disable-line @typescript-eslint/explicit-module-boundary-types
self.addEventListener('message', (e) => {
const data: any = e.data
switch (data.cmd) {
case 'load':
{
delete self.Module
// NOTE: workaround some browsers?
self.Module = undefined
// importScripts() method of synchronously imports one or more scripts into the worker's scope
self.importScripts(e.data.url)
// @ts-ignore
parser = SolidityParser as any;
self.postMessage({
cmd: 'loaded',
})
break
}
case 'parse':
if (data.text && parser) {
try {
let startTime = performance.now()
const blocks = parser.parseBlock(data.text, { loc: true, range: true, tolerant: true })
const blockDuration = performance.now() - startTime
startTime = performance.now()
const ast = parser.parse(data.text, { loc: true, range: true, tolerant: true })
const endTime = performance.now()
self.postMessage({
cmd: 'parsed',
timestamp: data.timestamp,
ast,
text: data.text,
file: data.file,
duration: endTime - startTime,
blockDuration,
blocks
})
} catch (e) {
// do nothing
}
}
break
}
}, false)
}

@ -3,18 +3,109 @@
import { AstNode } from "@remix-project/remix-solidity-ts"
import { CodeParser } from "../code-parser"
import { antlr } from '../types'
import work from 'webworkify-webpack'
const SolidityParser = (window as any).SolidityParser = (window as any).SolidityParser || []
interface BlockDefinition {
end: number
endColumn: number
endLine: number
name: string
parent: string
start: number
startColumn: number
startLine: number
type: string
}
export default class CodeParserAntlrService {
plugin: CodeParser
worker: Worker
parserStartTime: number = 0
workerTimer: NodeJS.Timer
parserTreshHold: number = 10
cache: {
[name: string]: {
text: string,
ast: antlr.ParseResult | null,
duration?: number,
blockDuration?: number,
parsingEnabled?: boolean,
blocks?: BlockDefinition[]
}
} = {};
constructor(plugin: CodeParser) {
this.plugin = plugin
this.createWorker()
}
/*
* simple parsing is used to quickly parse the current file or a text source without using the compiler or having to resolve imports
*/
createWorker() {
this.worker = work(require.resolve('./antlr-worker'));
this.worker.postMessage({
cmd: 'load',
url: document.location.protocol + '//' + document.location.host + '/assets/js/parser/antlr.js',
});
const self = this
this.worker.addEventListener('message', function (ev) {
switch (ev.data.cmd) {
case 'parsed':
if (ev.data.ast && self.parserStartTime === ev.data.timestamp) {
self.setFileParsingState(ev.data.file, ev.data.blockDuration)
self.cache[ev.data.file] = {
...self.cache[ev.data.file],
text: ev.data.text,
ast: ev.data.ast,
duration: ev.data.duration,
blockDuration: ev.data.blockDuration,
blocks: ev.data.blocks,
}
}
break;
}
});
}
setFileParsingState(file: string, duration: number) {
if (this.cache[file]) {
if (this.cache[file].blockDuration) {
if(this.cache[file].blockDuration > this.parserTreshHold && duration > this.parserTreshHold) {
this.cache[file].parsingEnabled = false
this.plugin.call('notification', 'toast', `This file is big so some autocomplete features will be disabled.`)
} else{
this.cache[file].parsingEnabled = true
}
}
}
}
enableWorker() {
if (!this.workerTimer) {
this.workerTimer = setInterval(() => {
this.getCurrentFileAST()
}, 5000)
}
}
disableWorker() {
clearInterval(this.workerTimer)
}
async parseWithWorker(text: string, file: string) {
this.parserStartTime = Date.now()
this.worker.postMessage({
cmd: 'parse',
text,
timestamp: this.parserStartTime,
file,
parsingEnabled: (this.cache[file] && this.cache[file].parsingEnabled) || true
});
}
async parseSolidity(text: string) {
const ast: antlr.ParseResult = (SolidityParser as any).parse(text, { loc: true, range: true, tolerant: true })
@ -28,17 +119,30 @@ export default class CodeParserAntlrService {
* @returns
*/
async getCurrentFileAST(text: string | null = null) {
this.plugin.currentFile = await this.plugin.call('fileManager', 'file')
if (this.plugin.currentFile && this.plugin.currentFile.endsWith('.sol')) {
if (!this.plugin.currentFile) return
const fileContent = text || await this.plugin.call('fileManager', 'readFile', this.plugin.currentFile)
try {
const ast = (SolidityParser as any).parse(fileContent, { loc: true, range: true, tolerant: true })
this.plugin.antlrParserResult = ast
} catch (e) {
// do nothing
try {
this.plugin.currentFile = await this.plugin.call('fileManager', 'file')
if (this.plugin.currentFile && this.plugin.currentFile.endsWith('.sol')) {
if (!this.plugin.currentFile) return
const fileContent = text || await this.plugin.call('fileManager', 'readFile', this.plugin.currentFile)
if (!this.cache[this.plugin.currentFile]) {
this.cache[this.plugin.currentFile] = {
text: '',
ast: null,
parsingEnabled: true
}
}
if (this.cache[this.plugin.currentFile] && this.cache[this.plugin.currentFile].text !== fileContent) {
try {
await this.parseWithWorker(fileContent, this.plugin.currentFile)
} catch (e) {
// do nothing
}
} else {
// do nothing
}
}
return this.plugin.antlrParserResult
} catch (e) {
// do nothing
}
}
@ -48,9 +152,10 @@ export default class CodeParserAntlrService {
* @returns
*/
async listAstNodes() {
await this.getCurrentFileAST();
this.plugin.currentFile = await this.plugin.call('fileManager', 'file')
if (!this.cache[this.plugin.currentFile]) return
const nodes: AstNode[] = [];
(SolidityParser as any).visit(this.plugin.antlrParserResult, {
(SolidityParser as any).visit(this.cache[this.plugin.currentFile].ast, {
StateVariableDeclaration: (node: antlr.StateVariableDeclaration) => {
if (node.variables) {
for (const variable of node.variables) {
@ -132,11 +237,19 @@ export default class CodeParserAntlrService {
*/
async getCurrentFileBlocks(text: string | null = null) {
this.plugin.currentFile = await this.plugin.call('fileManager', 'file')
if (this.cache[this.plugin.currentFile]) {
if (!this.cache[this.plugin.currentFile].parsingEnabled) {
return
}
}
if (this.plugin.currentFile && this.plugin.currentFile.endsWith('.sol')) {
if (!this.plugin.currentFile) return
const fileContent = text || await this.plugin.call('fileManager', 'readFile', this.plugin.currentFile)
try {
const startTime = Date.now()
const blocks = (SolidityParser as any).parseBlock(fileContent, { loc: true, range: true, tolerant: true })
this.setFileParsingState(this.plugin.currentFile, Date.now() - startTime)
if(blocks) this.cache[this.plugin.currentFile].blocks = blocks
return blocks
} catch (e) {
// do nothing
@ -152,11 +265,12 @@ export default class CodeParserAntlrService {
* @return {any}
* */
async getANTLRBlockAtPosition(position: any, text: string = null) {
const blocks = await this.getCurrentFileBlocks(text)
const blocks: any[] = await this.getCurrentFileBlocks(text)
const walkAst = (blocks) => {
let nodeFound = null
for(const object of blocks){
if(object.start <= position && object.end >= position){
for (const object of blocks) {
if (object.start <= position) {
nodeFound = object
break
}
@ -164,8 +278,8 @@ export default class CodeParserAntlrService {
return nodeFound
}
if (!blocks) return
const block = walkAst(blocks)
console.log(block)
blocks.reverse()
const block = walkAst(blocks)
return block
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -41,7 +41,7 @@
<script type="text/javascript" src="assets/js/loader.js"></script>
<script src="https://kit.fontawesome.com/41dd021e94.js" crossorigin="anonymous"></script>
<script type="text/javascript" src="assets/js/intro.min.js"></script>
<script type="text/javascript" src="assets/js/parser/index.iife.js"></script>
<script type="text/javascript" src="assets/js/parser/antlr.js"></script>
</body>
</html>

@ -1,4 +1,3 @@
import { sourceMappingDecoder } from "@remix-project/remix-debug"
import { AstNode } from "@remix-project/remix-solidity-ts"
import { isArray } from "lodash"
import { editor, languages, Position } from "monaco-editor"
@ -10,6 +9,7 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider
props: EditorUIProps
monaco: any
maximumItemsForContractCompletion = 100
constructor(props: any, monaco: any) {
this.props = props
@ -20,7 +20,8 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider
async provideCompletionItems(model: editor.ITextModel, position: Position, context: monaco.languages.CompletionContext): Promise<monaco.languages.CompletionList | undefined> {
const completionSettings = await this.props.plugin.call('config', 'getAppParameter', 'settings/auto-completion')
if(!completionSettings) return
if (!completionSettings) return
const word = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
@ -41,28 +42,24 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider
let dotCompleted = false
// handles completion from for builtin types
if(lastNodeInExpression.memberName === 'sender') { // exception for this member
if (lastNodeInExpression.memberName === 'sender') { // exception for this member
lastNodeInExpression.name = 'sender'
}
const globalCompletion = getContextualAutoCompleteByGlobalVariable(lastNodeInExpression.name, range, this.monaco)
if (globalCompletion) {
dotCompleted = true
suggestions = [...suggestions, ...globalCompletion]
setTimeout(() => {
// eslint-disable-next-line no-debugger
// debugger
}, 2000)
}
// handle completion for global THIS.
if (lastNodeInExpression.name === 'this') {
dotCompleted = true
nodes = [...nodes, ...await this.getThisCompletions(position)]
nodes = [...nodes, ...await this.getThisCompletions()]
}
// handle completion for other dot completions
if (expressionElements.length > 1 && !dotCompleted) {
const nameOfLastTypedExpression = lastNodeInExpression.name || lastNodeInExpression.memberName
const dotCompletions = await this.getDotCompletions(position, nameOfLastTypedExpression, range)
const dotCompletions = await this.getDotCompletions(nameOfLastTypedExpression, range)
nodes = [...nodes, ...dotCompletions.nodes]
suggestions = [...suggestions, ...dotCompletions.suggestions]
}
@ -77,7 +74,8 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider
...GetGlobalFunctions(range, this.monaco),
...GeCompletionUnits(range, this.monaco),
]
let contractCompletions = await this.getContractCompletions(position)
let contractCompletions = await this.getContractCompletions()
// we can't have external nodes without using this.
contractCompletions = contractCompletions.filter(node => {
@ -86,13 +84,14 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider
}
return true
})
nodes = [...nodes, ...contractCompletions]
}
// remove duplicates
const nodeIds = {};
const filteredNodes = nodes.filter((node) => {
let filteredNodes = nodes.filter((node) => {
if (node.id) {
if (nodeIds[node.id]) {
return false;
@ -102,6 +101,12 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider
return true;
});
// truncate for performance
if (filteredNodes.length > this.maximumItemsForContractCompletion) {
await this.props.plugin.call('notification', 'toast', `Too many completion items. Only ${this.maximumItemsForContractCompletion} items will be shown.`)
filteredNodes = filteredNodes.slice(0, this.maximumItemsForContractCompletion)
}
const getNodeLink = async (node: any) => {
return await this.props.plugin.call('codeParser', 'getNodeLink', node)
}
@ -220,7 +225,7 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider
}
suggestions.push(completion)
}
}
}
return {
@ -228,68 +233,13 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider
}
}
private getBlockNodesAtPosition = async (position: Position, ANTLRBlock) => {
let nodes: any[] = []
const cursorPosition = this.props.editorAPI.getCursorPosition()
const nodesAtPosition = await this.props.plugin.call('codeParser', 'nodesAtPosition', cursorPosition)
// if the block has a name and a type we can maybe find it in the contract nodes
const fileNodes = await this.props.plugin.call('codeParser', 'getCurrentFileNodes')
if (isArray(nodesAtPosition) && nodesAtPosition.length) {
for (const node of nodesAtPosition) {
// try to find the real block in the AST and get the nodes in that scope
if (node.nodeType === 'ContractDefinition') {
const contractNodes = fileNodes.contracts[node.name].contractNodes
for (const contractNode of Object.values(contractNodes)) {
if (contractNode['name'] === ANTLRBlock.name
|| (contractNode['kind'] === 'constructor' && ANTLRBlock.name === null )
) {
let nodeOfScope = await this.props.plugin.call('codeParser', 'getNodesWithScope', (contractNode as any).id)
nodes = [...nodes, ...nodeOfScope]
if (contractNode['body']) {
nodeOfScope = await this.props.plugin.call('codeParser', 'getNodesWithScope', (contractNode['body'] as any).id)
nodes = [...nodes, ...nodeOfScope]
}
}
}
}
// blocks can have statements
/*
if (node.statements){
console.log('statements', node.statements)
for (const statement of node.statements) {
if(statement.expression){
const declaration = await this.props.plugin.call('codeParser', 'declarationOf', statement.expression)
declaration.outSideBlock = true
nodes = [...nodes, declaration]
}
}
}
*/
}
}
// we are only interested in nodes that are in the same block as the cursor
nodes = nodes.filter(node => {
if (node.src) {
const position = sourceMappingDecoder.decode(node.src)
if (position.start >= ANTLRBlock.start && (position.start + position.length) <= ANTLRBlock.end) {
return true
}
}
if(node.outSideBlock){ return true }
return false
})
return nodes;
}
private getContractCompletions = async (position: Position) => {
private getContractCompletions = async () => {
let nodes: any[] = []
const cursorPosition = this.props.editorAPI.getCursorPosition()
let nodesAtPosition = await this.props.plugin.call('codeParser', 'nodesAtPosition', cursorPosition)
// if no nodes exits at position, try to get the block of which the position is in
const block = await this.props.plugin.call('codeParser', 'getANTLRBlockAtPosition', cursorPosition, null)
const fileNodes = await this.props.plugin.call('codeParser', 'getCurrentFileNodes')
if (!nodesAtPosition.length) {
if (block) {
nodesAtPosition = await this.props.plugin.call('codeParser', 'nodesAtPosition', block.start)
@ -301,13 +251,26 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider
for (const node of nodesAtPosition) {
if (node.nodeType === 'ContractDefinition') {
contractNode = node
const fileNodes = await this.props.plugin.call('codeParser', 'getCurrentFileNodes')
const contractNodes = fileNodes.contracts[node.name]
nodes = [...Object.values(contractNodes.contractScopeNodes), ...nodes]
nodes = [...Object.values(contractNodes.baseNodesWithBaseContractScope), ...nodes]
nodes = [...Object.values(fileNodes.imports), ...nodes]
// at the nodes at the block itself
nodes = [...nodes, ...await this.getBlockNodesAtPosition(position, block)]
// add the nodes at the block itself
if (node.nodeType === 'ContractDefinition' && block && block.name) {
const contractNodes = fileNodes.contracts[node.name].contractNodes
for (const contractNode of Object.values(contractNodes)) {
if (contractNode['name'] === block.name
|| (contractNode['kind'] === 'constructor' && block.name === 'constructor')
) {
let nodeOfScope = await this.props.plugin.call('codeParser', 'getNodesWithScope', (contractNode as any).id)
nodes = [...nodes, ...nodeOfScope]
if (contractNode['body']) {
nodeOfScope = await this.props.plugin.call('codeParser', 'getNodesWithScope', (contractNode['body'] as any).id)
nodes = [...nodes, ...nodeOfScope]
}
}
}
}
// filter private nodes, only allow them when contract ID is the same as the current contract
nodes = nodes.filter(node => {
if (node.visibility) {
@ -323,14 +286,17 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider
}
} else {
// get all the nodes from a simple code parser which only parses the current file
nodes = [...nodes, ...await this.props.plugin.call('codeParser', 'listAstNodes')]
const allNodesFromAntlr = await this.props.plugin.call('codeParser', 'listAstNodes')
if (allNodesFromAntlr) {
nodes = [...nodes, ...allNodesFromAntlr]
}
}
return nodes
}
private getThisCompletions = async (position: Position) => {
private getThisCompletions = async () => {
let nodes: any[] = []
let thisCompletionNodes = await this.getContractCompletions(position)
let thisCompletionNodes = await this.getContractCompletions()
const allowedTypesForThisCompletion = ['VariableDeclaration', 'FunctionDefinition']
// with this. you can't have internal nodes and no contractDefinitions
thisCompletionNodes = thisCompletionNodes.filter(node => {
@ -343,15 +309,11 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider
return true
})
nodes = [...nodes, ...thisCompletionNodes]
setTimeout(() => {
// eslint-disable-next-line no-debugger
// debugger
}, 2000)
return nodes
}
private getDotCompletions = async (position: Position, nameOfLastTypedExpression: string, range) => {
const contractCompletions = await this.getContractCompletions(position)
private getDotCompletions = async (nameOfLastTypedExpression: string, range) => {
const contractCompletions = await this.getContractCompletions()
let nodes: any[] = []
let suggestions: monaco.languages.CompletionItem[] = []
@ -385,9 +347,9 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider
nodes = [...nodes, ...filterNodes(nodeOfScope.members, nodeOfScope)]
} else if (nodeOfScope.typeName && nodeOfScope.typeName.nodeType === 'ArrayTypeName') {
suggestions = [...suggestions, ...getContextualAutoCompleteBTypeName('ArrayTypeName', range, this.monaco)]
} else if(nodeOfScope.typeName && nodeOfScope.typeName.nodeType === 'ElementaryTypeName' && nodeOfScope.typeName.name === 'bytes') {
} else if (nodeOfScope.typeName && nodeOfScope.typeName.nodeType === 'ElementaryTypeName' && nodeOfScope.typeName.name === 'bytes') {
suggestions = [...suggestions, ...getContextualAutoCompleteBTypeName('bytes', range, this.monaco)]
} else if(nodeOfScope.typeName && nodeOfScope.typeName.nodeType === 'ElementaryTypeName' && nodeOfScope.typeName.name === 'address') {
} else if (nodeOfScope.typeName && nodeOfScope.typeName.nodeType === 'ElementaryTypeName' && nodeOfScope.typeName.name === 'address') {
suggestions = [...suggestions, ...getContextualAutoCompleteBTypeName('address', range, this.monaco)]
}
}

@ -14,7 +14,7 @@ export class RemixDefinitionProvider implements monaco.languages.DefinitionProvi
async provideDefinition(model: monaco.editor.ITextModel, position: monaco.Position, token: monaco.CancellationToken): Promise<monaco.languages.Definition | monaco.languages.LocationLink[]> {
const cursorPosition = this.props.editorAPI.getCursorPosition()
const jumpLocation = await this.jumpToDefinition(cursorPosition)
if(!jumpLocation || !jumpLocation.fileName) return []
return [{
uri: this.monaco.Uri.parse(jumpLocation.fileName),
range: {

@ -103,7 +103,7 @@ export interface EditorUIProps {
findMatches: (uri: string, value: string) => any
getFontSize: () => number,
getValue: (uri: string) => string
getCursorPosition: () => number
getCursorPosition: (offset?: boolean) => number | IPosition
getHoverPosition: (position: IPosition) => number
addDecoration: (marker: sourceMarker, filePath: string, typeOfDecoration: string) => DecorationsReturn
clearDecorationsByPlugin: (filePath: string, plugin: string, typeOfDecoration: string, registeredDecorations: any, currentDecorations: any) => DecorationsReturn
@ -476,11 +476,11 @@ export const EditorUI = (props: EditorUIProps) => {
}
}
props.editorAPI.getCursorPosition = () => {
props.editorAPI.getCursorPosition = (offset:boolean = true) => {
if (!monacoRef.current) return
const model = editorModelsState[currentFileRef.current]?.model
if (model) {
return model.getOffsetAt(editorRef.current.getPosition())
return offset? model.getOffsetAt(editorRef.current.getPosition()): editorRef.current.getPosition()
}
}

Loading…
Cancel
Save