From df9594b445d3fbda435ca0ffdc0af1a749414e0a Mon Sep 17 00:00:00 2001 From: Jonas Jing <932166095@qq.com> Date: Tue, 5 Dec 2023 16:41:01 +0800 Subject: [PATCH] feat: recursive Folding Block (#555) --- server/src/sas/CodeZoneManager.ts | 11 +- server/src/sas/FormatOnTypeProvider.ts | 8 +- server/src/sas/LanguageServiceProvider.ts | 149 ++++--- server/src/sas/LexerEx.ts | 478 +++++++++++++++++----- server/src/sas/SyntaxProvider.ts | 10 +- server/src/server.ts | 22 +- 6 files changed, 513 insertions(+), 165 deletions(-) diff --git a/server/src/sas/CodeZoneManager.ts b/server/src/sas/CodeZoneManager.ts index 14fa4fa27..c5c61580b 100644 --- a/server/src/sas/CodeZoneManager.ts +++ b/server/src/sas/CodeZoneManager.ts @@ -808,6 +808,9 @@ export class CodeZoneManager { block = this._syntaxProvider.getFoldingBlock( tmpContext.line, tmpContext.col, + false, + true, + true, ); if (block) { if (this._inBlock(block, token)! < 0 && !this._endedReally(block)) { @@ -2428,7 +2431,13 @@ export class CodeZoneManager { const tmpLine = pos.line, tmpCol = pos.col; let token = this._token(tmpLine, tmpCol)!; - const block = this._syntaxProvider.getFoldingBlock(tmpLine, tmpCol); + const block = this._syntaxProvider.getFoldingBlock( + tmpLine, + tmpCol, + false, + true, + true, + ); /* first check type to determine zone, some special conditions * 1) for bringing up auto completion popup by shortcut, * 2) input at the end of a line in comment or literal diff --git a/server/src/sas/FormatOnTypeProvider.ts b/server/src/sas/FormatOnTypeProvider.ts index da04d974f..55c9ffa43 100644 --- a/server/src/sas/FormatOnTypeProvider.ts +++ b/server/src/sas/FormatOnTypeProvider.ts @@ -95,7 +95,13 @@ export class FormatOnTypeProvider { } // Otherwise, need to decrease indent of current line const foldingBlock: FoldingBlock | null = - this.syntaxProvider.getFoldingBlock(line, semicolonCol); + this.syntaxProvider.getFoldingBlock( + line, + semicolonCol, + false, + true, + true, + ); let blockStartLine; if (!foldingBlock) { const lastNotEmptyLine = this._getLastNotEmptyLine(line - 1); diff --git a/server/src/sas/LanguageServiceProvider.ts b/server/src/sas/LanguageServiceProvider.ts index 5c22b135b..e50f96d11 100644 --- a/server/src/sas/LanguageServiceProvider.ts +++ b/server/src/sas/LanguageServiceProvider.ts @@ -1,10 +1,12 @@ // Copyright © 2022, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { TextDocument } from "vscode-languageserver-textdocument"; +import { FoldingRange } from "vscode-languageserver"; +import { Range, TextDocument } from "vscode-languageserver-textdocument"; import { DocumentSymbol, SymbolKind } from "vscode-languageserver-types"; import { CompletionProvider } from "./CompletionProvider"; import { FormatOnTypeProvider } from "./FormatOnTypeProvider"; +import { FoldingBlock } from "./LexerEx"; import { Model } from "./Model"; import type { LibService } from "./SyntaxDataProvider"; import { SyntaxProvider } from "./SyntaxProvider"; @@ -38,8 +40,14 @@ function getType(type: string) { return legend.tokenTypes.indexOf(type); } -// DATA, PROC, MACRO -const SymbolKinds = [SymbolKind.Struct, SymbolKind.Function, SymbolKind.Module]; +// DATA, PROC, MACRO, GBL, CUSTOM +const SymbolKinds = [ + SymbolKind.Struct, + SymbolKind.Function, + SymbolKind.Module, + SymbolKind.Module, + SymbolKind.Module, +]; export class LanguageServiceProvider { private model; @@ -118,65 +126,108 @@ export class LanguageServiceProvider { return data; } - getFoldingBlocks(): DocumentSymbol[] { + getDocumentSymbols(): DocumentSymbol[] { const lineCount = this.model.getLineCount(); - const result = []; - let customBlock; + const result: DocumentSymbol[] = []; for (let i = 0; i < lineCount; i++) { - const block = this.syntaxProvider.getFoldingBlock(i); - - if (block && block.startLine === i) { - const range = { - start: { line: block.startLine, character: block.startCol }, - end: { line: block.endFoldingLine, character: block.endFoldingCol }, - }; - result.push({ - name: - block.type === 1 ? this._getProcName(block.startLine) : block.name, - kind: SymbolKinds[block.type], - range, - selectionRange: range, - }); - i = block.endFoldingLine; + const rootBlock = this.syntaxProvider.getFoldingBlock( + i, + undefined, + false, + false, + true, + ); + if (rootBlock && rootBlock.startLine === i) { + const docSymbol: DocumentSymbol = this._buildDocumentSymbol(rootBlock); + result.push(docSymbol); + i = rootBlock.endFoldingLine; continue; } - let token = this.syntaxProvider.getSyntax(i)[0]; - if (token && token.style === "text") { - token = this.syntaxProvider.getSyntax(i)[1]; - } - if (token && /comment/.test(token.style)) { - if (/^\s*[%/]?\*\s*region\b/i.test(this.model.getLine(i))) { - customBlock = { - start: { line: i, character: 0 }, - end: { - line: this.model.getLineCount(), - character: 0, - }, - }; - } else if ( - customBlock && - /^\s*[%/]?\*\s*endregion\b/i.test(this.model.getLine(i)) - ) { - customBlock.end = { - line: i, - character: this.model.getColumnCount(i), - }; + } + return result; + } + + private _buildDocumentSymbol( + block: FoldingBlock, + parent?: DocumentSymbol, + ): DocumentSymbol { + const range: Range = { + start: { line: block.startLine, character: block.startCol }, + end: { line: block.endFoldingLine, character: block.endFoldingCol }, + }; + const docSymbol: DocumentSymbol = { + name: block.type === 1 ? this._getProcName(block.startLine) : block.name, + kind: SymbolKinds[block.type], + range, + selectionRange: range, + children: [], + }; + if (parent) { + parent.children!.push(docSymbol); + } + for (const innerBlock of block.innerBlocks) { + this._buildDocumentSymbol(innerBlock, docSymbol); + } + return docSymbol; + } + + getFoldingRanges(): FoldingRange[] { + const lineCount = this.model.getLineCount(); + const result: FoldingRange[] = []; + + for (let i = 0; i < lineCount; i++) { + const rootBlock = this.syntaxProvider.getFoldingBlock( + i, + undefined, + false, + false, + true, + ); + if (rootBlock && rootBlock.startLine === i) { + const blocks: FoldingBlock[] = this._flattenFoldingBlockTree(rootBlock); + for (const block of blocks) { result.push({ - name: "custom", - kind: SymbolKind.Module, - range: customBlock, - selectionRange: customBlock, + startLine: block.startLine, + endLine: block.endFoldingLine, }); - customBlock = undefined; } + i = rootBlock.endFoldingLine; + continue; } } return result; } - getFoldingBlock(line: number, col: number) { - return this.syntaxProvider.getFoldingBlock(line, col, true); + // DFS + private _flattenFoldingBlockTree(rootBlock: FoldingBlock): FoldingBlock[] { + const stack: FoldingBlock[] = [rootBlock]; + const resultList: FoldingBlock[] = []; + while (stack.length > 0) { + const curBlock: FoldingBlock = stack.pop()!; + resultList.push(curBlock); + for (let i = curBlock.innerBlocks.length - 1; i >= 0; i--) { + const innerBlock = curBlock.innerBlocks[i]; + stack.push(innerBlock); + } + } + return resultList; + } + + getFoldingBlock( + line: number, + col: number, + strict?: boolean, + ignoreCustomBlock?: boolean, + ignoreGlobalBlock?: boolean, + ) { + return this.syntaxProvider.getFoldingBlock( + line, + col, + strict, + ignoreCustomBlock, + ignoreGlobalBlock, + ); } setLibService(fn: LibService): void { diff --git a/server/src/sas/LexerEx.ts b/server/src/sas/LexerEx.ts index 04365818c..93c5cf6ab 100644 --- a/server/src/sas/LexerEx.ts +++ b/server/src/sas/LexerEx.ts @@ -27,6 +27,8 @@ export class FoldingBlock { specialBlks: any; sectionIdx: number | undefined; blockComment: boolean | undefined; + outerBlock?: FoldingBlock; + innerBlocks: FoldingBlock[]; constructor(...arg: any[]) { if (arguments.length === 1) { //copy constructor @@ -38,6 +40,8 @@ export class FoldingBlock { this.name = arguments[0].name; this.endFoldingLine = arguments[0].endFoldingLine; this.endFoldingCol = arguments[0].endFoldingCol; + this.outerBlock = arguments[0].outerBlock; + this.innerBlocks = [...arguments[0].innerBlocks]; } else if (arguments.length >= 4) { this.startLine = arguments[0]; this.startCol = arguments[1]; @@ -45,6 +49,7 @@ export class FoldingBlock { this.endCol = arguments[3]; this.type = arguments[4]; this.name = arguments[5] ? arguments[5] : ""; + this.innerBlocks = []; this.endFoldingLine = -1; this.endFoldingCol = -1; } else { @@ -56,6 +61,7 @@ export class FoldingBlock { this.name = ""; this.endFoldingLine = -1; this.endFoldingCol = -1; + this.innerBlocks = []; } } } @@ -106,7 +112,7 @@ export class LexerEx { lexer: Lexer; expr: Expression; syntaxDb: SyntaxDataProvider; - SEC_TYPE: any; + SEC_TYPE: typeof LexerEx.SEC_TYPE; PARSING_STATE: { IN_GBL: number; IN_MACRO: number; @@ -127,11 +133,9 @@ export class LexerEx { lookAheadTokens: any[]; sectionCache: (FoldingBlock | null)[]; lastToken: any; - - blockDepth = 0; sections: FoldingBlock[] = []; tailSections: FoldingBlock[] = []; - currSection: FoldingBlock; + currSection?: FoldingBlock; //stmts: any[] = [], //currStmt, //isStmtStart = true, @@ -146,7 +150,9 @@ export class LexerEx { PROC: 1, MACRO: 2, GBL: 3, + CUSTOM: 4, }; + constructor(private model: Model) { this.lexer = new Lexer(model); this.expr = new Expression(this); @@ -202,7 +208,7 @@ export class LexerEx { if ( !block || - this._isBetween(pos, this._startPos(block), this._endPos(block)) + this._isBetween(pos, this._startPos(block), this._endPos(block), true) ) { return currentIdx; } @@ -214,9 +220,12 @@ export class LexerEx { do { i--; block = blocks[i]; - } while (block && !this._isBefore(this._endPos(block), pos)); // []| + } while ( + block && + !this._isBetween(pos, this._startPos(block), this._endPos(block), true) + ); // []| - return ++i; + return block ? i : -1; } else { /* []| -> @@ -224,8 +233,11 @@ export class LexerEx { do { i++; block = blocks[i]; - } while (block && !this._isBefore(pos, this._startPos(block))); // |[] - return --i; + } while ( + block && + !this._isBetween(pos, this._startPos(block), this._endPos(block), true) + ); // |[] + return block ? i : -1; } } @@ -383,18 +395,30 @@ export class LexerEx { pos: TextPosition, start: TextPosition, end: TextPosition, + inclusive?: boolean, ) { - return this._isBefore(start, pos) && this._isBefore(pos, end); + return ( + this._isBefore(start, pos, inclusive) && + this._isBefore(pos, end, inclusive) + ); } - private _isBefore(pos1: TextPosition, pos2: TextPosition) { + + private _isBefore( + pos1: TextPosition, + pos2: TextPosition, + inclusive?: boolean, + ) { if (pos1.line < pos2.line) { return true; } else if (pos1.line === pos2.line) { - if (pos1.column < pos2.column) { - return true; + if (inclusive) { + return pos1.column <= pos2.column; + } else { + return pos1.column < pos2.column; } + } else { + return false; } - return false; } private _getBlkIndex( @@ -589,20 +613,34 @@ export class LexerEx { prevBlock.endFoldingCol = prevBlock.endCol; } } - private push_(block: FoldingBlock) { - this.trimBlock_(block); - // folding end - this.sectionCache[block.startLine] = null; // clear cached block, it must try to get the last - block.endFoldingLine = block.endLine; - block.endFoldingCol = block.endCol; + private _pushRootBlock(block: FoldingBlock) { // adjujst previous block - if (this.sections.length) { - this.adjustFoldingEnd_(this.sections[this.sections.length - 1], block); + const stack: FoldingBlock[] = [block]; + while (stack.length > 0) { + const curBlock: FoldingBlock = stack.pop()!; + for (let i = curBlock.innerBlocks.length - 1; i >= 0; i--) { + const innerBlock = curBlock.innerBlocks[i]; + stack.push(innerBlock); + } } + this._adjustBlockTreeFoldingEnd(block); // add this.sections.push(block); this.tokens = []; } + + private _adjustBlockTreeFoldingEnd(rootBlock: FoldingBlock): void { + for (let i = 1; i < rootBlock.innerBlocks.length; i++) { + this.adjustFoldingEnd_( + rootBlock.innerBlocks[i - 1], + rootBlock.innerBlocks[i], + ); + } + for (let i = 0; i < rootBlock.innerBlocks.length; i++) { + this._adjustBlockTreeFoldingEnd(rootBlock.innerBlocks[i]); + } + } + //TODO: IMPROVE private _changeCardsDataToken(token: { type: string; @@ -617,15 +655,21 @@ export class LexerEx { return { line: pos.line, column: pos.column }; } private startFoldingBlock_(type: any, pos: TextPosition, name: string) { - this.blockDepth++; - if (this.blockDepth === 1) { - this.currSection.startLine = pos.line; - this.currSection.startCol = pos.column; - this.currSection.type = type; - this.currSection.name = name; - this.currSection.specialBlks = null; + const newBlock = new FoldingBlock(); + newBlock.startLine = pos.line; + newBlock.startCol = pos.column; + newBlock.type = type; + newBlock.name = name; + newBlock.specialBlks = null; + if (!this.currSection) { + this.currSection = newBlock; + } else { + this.currSection.innerBlocks.push(newBlock); + newBlock.outerBlock = this.currSection; + this.currSection = newBlock; } } + private endFoldingBlock_( type: any, pos: TextPosition, @@ -633,42 +677,44 @@ export class LexerEx { start?: { start: any }, name?: string, ) { - // positively end - let add = false; - this.blockDepth--; - if (this.blockDepth === 0) { - add = true; - } - if (add) { - if (pos.line >= this.currSection.startLine) { - this.currSection.endLine = pos.line; - this.currSection.endCol = pos.column; - //currSection.type = type; - const block = new FoldingBlock(this.currSection); - //var block = {}; - //jQuery.extend(true, block, currSection); - block.explicitEnd = explicitEnd; + if (!this.currSection) { + return; + } + if (pos.line >= this.currSection.startLine) { + this.currSection.endLine = pos.line; + this.currSection.endCol = pos.column; + this.trimBlock_(this.currSection); + // folding end + this.sectionCache[this.currSection.startLine] = null; // clear cached block, it must try to get the last + this.currSection.endFoldingLine = this.currSection.endLine; + this.currSection.endFoldingCol = this.currSection.endCol; + //currSection.type = type; + if (!this.currSection.outerBlock) { + this.currSection.explicitEnd = explicitEnd; if (explicitEnd) { - block.explicitEndStmt = {}; - block.explicitEndStmt.start = start?.start; - block.explicitEndStmt.name = name; + this.currSection.explicitEndStmt = {}; + this.currSection.explicitEndStmt.start = start?.start; + this.currSection.explicitEndStmt.name = name; } if (this.currSection.specialBlks) { - block.specialBlks = this.currSection.specialBlks; + this.currSection.specialBlks = this.currSection.specialBlks; this.currSection.specialBlks = null; } - this.push_(block); + this._pushRootBlock(this.currSection); } - - this.currSection.startLine = -1; } + this.currSection = this.currSection.outerBlock; } - private hasFoldingBlock_() { - return this.currSection.startLine >= 0; + private hasFoldingBlock_(): boolean { + return !!this.currSection; } - private getLastNormalFoldingBlockInLine_(currentIdx: number, line: number) { + private getLastNormalFoldingBlockInLine_( + currentIdx: number, + line: number, + blocks: FoldingBlock[], + ): FoldingBlock | null { let i = currentIdx, - block = this.sections[i], + block = blocks[i], idx = currentIdx; // find forward while (block && (block.startLine === line || block.endLine === line)) { @@ -676,72 +722,99 @@ export class LexerEx { idx = i; } else idx = -1; i++; - block = this.sections[i]; + block = blocks[i]; } // find backward if (idx < 0) { i = currentIdx - 1; - block = this.sections[i]; + block = blocks[i]; while (block && (block.startLine === line || block.endLine === line)) { if (block.type !== this.SEC_TYPE.GBL) { idx = i; break; } else idx = -1; i--; - block = this.sections[i]; + block = blocks[i]; } } - // ignore global - if (this.sections[idx] && this.sections[idx].type === this.SEC_TYPE.GBL) { - idx = -1; - } - return this.sections[idx]; + return blocks[idx] ?? null; //return sections[idx]?sections[idx]:null; //we return null if no } - private getFoldingBlock_(line: number, col?: number, strict?: boolean) { - const idx = this.getBlockPos_(this.sections, line, col); - let block = this.sections[idx]; + private getFoldingBlock_( + blocks: FoldingBlock[], + line: number, + col?: number, + strict?: boolean, + ): FoldingBlock | null { + const idx = this.getBlockPos_(blocks, line, col); + let block: FoldingBlock | null = blocks[idx]; + let found: boolean = false; if (strict) { - return block; - } - if (block && block.startLine <= line && block.endLine >= line) { - return this.getLastNormalFoldingBlockInLine_(idx, line); + found = !!block; + } else if (block && block.startLine <= line && block.endLine >= line) { + block = this.getLastNormalFoldingBlockInLine_(idx, line, blocks); + found = !!block; } else if (col) { // for last block, the input position is the last - block = this.sections[this.sections.length - 1]; + block = blocks[blocks.length - 1]; if ( block && !this._isBefore({ line: line, column: col }, this._endPos(block)) && !block.explicitEnd ) { // must use ! + found = true; + } + } + if (found) { + if (block!.innerBlocks.length > 0) { + return ( + this.getFoldingBlock_(block!.innerBlocks, line, col, strict) ?? block + ); + } else { return block; } + } else { + return null; } - return null; } - getFoldingBlock(line: number, col?: number, strict?: boolean) { + getFoldingBlock( + line: number, + col?: number, + strict?: boolean, + ignoreCustomBlock?: boolean, + ignoreGlobalBlock?: boolean, + ): FoldingBlock | null { + let block: FoldingBlock | null = null; if (col === undefined) { if (!this.sectionCache[line]) { - const section = this.getFoldingBlock_(line); + const section = this.getFoldingBlock_(this.sections, line); if (section && line <= section.endFoldingLine!) { this.sectionCache[line] = section; } else { this.sectionCache[line] = null; } } - return this.sectionCache[line]; + block = this.sectionCache[line]; + } else { + block = this.getFoldingBlock_(this.sections, line, col, strict); + } + while ( + (ignoreCustomBlock && block?.type === this.SEC_TYPE.CUSTOM) || + (ignoreGlobalBlock && block?.type === this.SEC_TYPE.GBL) + ) { + block = block.outerBlock ?? null; } - return this.getFoldingBlock_(line, col, strict); + return block; } private getBlockPos_(blocks: FoldingBlock[], line: number, col?: number) { let idx = this.getBlockPos1_(blocks, line); if (col || col === 0) { idx = this.getBlockPos2_(blocks, idx, line, col); // multiple blocks are in one same lines } - return idx; } + //SUPPORT CODE FOLDING //we define global statments as a kind of block, so the return will always be the first form. // @@ -778,9 +851,15 @@ export class LexerEx { } private resetFoldingBlockCache_() { this.sections = []; - this.blockDepth = 0; } - private tryEndFoldingBlock_(pos: TextPosition) { + private tryEndFoldingBlock_( + pos: TextPosition, + untilType?: number, + lastPos?: TextPosition, + ) { + if (!lastPos) { + lastPos = pos; + } if (this.hasFoldingBlock_()) { // handle text end let secType = this.SEC_TYPE.PROC; @@ -791,11 +870,12 @@ export class LexerEx { } else if (this.curr.state === this.PARSING_STATE.IN_GBL) { secType = this.SEC_TYPE.GBL; } - this.endFoldingBlock_(secType, pos); - - while (this.blockDepth > 0) { - this.endFoldingBlock_(secType, pos); - this.blockDepth--; + let stop = false; + while (this.currSection && !stop) { + if (untilType && this.currSection.type === untilType) { + stop = true; + } + this.endFoldingBlock_(secType, stop ? lastPos : pos); } } } @@ -810,7 +890,7 @@ export class LexerEx { !this._isBefore(token.start, this._startPos(this.tailSections[0])) ) { if (this.hasFoldingBlock_()) { - if (this.currSection.type === this.SEC_TYPE.MACRO) { + if (this.currSection?.type === this.SEC_TYPE.MACRO) { this.tailSections.splice(0, 1); return; } @@ -1163,6 +1243,7 @@ export class LexerEx { } } } + private start_(change: Change) { const parseRange = this.getParseRangeBySections_(change); @@ -1173,7 +1254,7 @@ export class LexerEx { this.stack = [{ parse: this.readProg_, state: this.PARSING_STATE.IN_GBL }]; this.curr = null; - this.blockDepth = 0; + this.currSection = undefined; this.sectionCache.splice( parseRange.startLine, this.sectionCache.length - parseRange.startLine, @@ -1181,6 +1262,7 @@ export class LexerEx { this.lexer.startFrom(parseRange.startLine, parseRange.startCol); return parseRange; } + private _handleSections(change: Change, parseRange: any) { // keep the blocks not changed this.tailSections = this.sections; @@ -1418,6 +1500,10 @@ export class LexerEx { return token; } private addTknBlock_(block: FoldingBlock) { + if (!this.currSection) { + // unexpected + return; + } if (!this.currSection.specialBlks) { //const with quotes, comment, cards data this.currSection.specialBlks = []; @@ -1610,6 +1696,21 @@ export class LexerEx { } return false; } + + private isCustomBlockStart_(token: Token): boolean { + if (token && /^\s*[%/]?\*\s*region\b/i.test(token.text)) { + return true; + } + return false; + } + + private isCustomBlockEnd_(token: Token): boolean { + if (token && /^\s*[%/]?\*\s*endregion\b/i.test(token.text)) { + return true; + } + return false; + } + private readProg_() { let word = "", gbl = true, @@ -1673,7 +1774,19 @@ export class LexerEx { } if (gbl) { - this.startFoldingBlock_(this.SEC_TYPE.GBL, token.start, word); + if (Lexer.isComment[token.type]) { + if (this.isCustomBlockStart_(token)) { + this.startFoldingBlock_( + this.SEC_TYPE.CUSTOM, + token.start, + token.text, + ); + } else if (this.isCustomBlockEnd_(token)) { + this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); + } + } else if (!this.hasFoldingBlock_()) { + this.startFoldingBlock_(this.SEC_TYPE.GBL, token.start, word); + } this.stack.push({ parse: this.readGbl_, state: this.PARSING_STATE.IN_GBL, @@ -2463,7 +2576,11 @@ export class LexerEx { break; case "PROC": case "PROCEDURE": { - this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + if (this.currSection?.type === this.SEC_TYPE.CUSTOM) { + this.tryPromoteCustomBlock_(); + } else { + this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + } this.startFoldingBlock_(this.SEC_TYPE.PROC, token.start, word); token.type = Lexer.TOKEN_TYPES.SKEYWORD; this.popSMTo_(1); @@ -2481,7 +2598,11 @@ export class LexerEx { break; } case "DATA": - this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + if (this.currSection?.type === this.SEC_TYPE.CUSTOM) { + this.tryPromoteCustomBlock_(); + } else { + this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + } this.startFoldingBlock_(this.SEC_TYPE.DATA, token.start, word); token.type = Lexer.TOKEN_TYPES.SKEYWORD; this.popSMTo_(1); @@ -2495,7 +2616,11 @@ export class LexerEx { }); break; case "%MACRO": - this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + if (this.currSection?.type === this.SEC_TYPE.CUSTOM) { + this.tryPromoteCustomBlock_(); + } else { + this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + } this.startFoldingBlock_(this.SEC_TYPE.MACRO, token.start, word); token.type = Lexer.TOKEN_TYPES.MSKEYWORD; this.popSMTo_(1); @@ -2811,7 +2936,6 @@ export class LexerEx { this.stack[this.stack.length - 2].state === this.PARSING_STATE.IN_MACRO ) { - this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); // end this proc this.stack.pop(); this.stack.push({ parse: this.readMend_, @@ -2826,7 +2950,11 @@ export class LexerEx { case "PROCEDURE": { //no normal end, and another proc meet, there are syntax errors // ignore - this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + if (this.currSection?.type === this.SEC_TYPE.CUSTOM) { + this.tryPromoteCustomBlock_(); + } else { + this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + } this.startFoldingBlock_(this.SEC_TYPE.PROC, token.start, word); token.type = Lexer.TOKEN_TYPES.SKEYWORD; @@ -2845,7 +2973,11 @@ export class LexerEx { break; } case "%MACRO": - this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + if (this.currSection?.type === this.SEC_TYPE.CUSTOM) { + this.tryPromoteCustomBlock_(); + } else { + this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + } this.startFoldingBlock_(this.SEC_TYPE.MACRO, token.start, word); token.type = Lexer.TOKEN_TYPES.MSKEYWORD; @@ -2861,7 +2993,11 @@ export class LexerEx { break; case "DATA": if (!this.DS2_[procName]) { - this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + if (this.currSection?.type === this.SEC_TYPE.CUSTOM) { + this.tryPromoteCustomBlock_(); + } else { + this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + } this.startFoldingBlock_(this.SEC_TYPE.DATA, token.start, word); token.type = Lexer.TOKEN_TYPES.SKEYWORD; @@ -2945,6 +3081,21 @@ export class LexerEx { } } } + } else if (Lexer.isComment[token.type]) { + if (this.isCustomBlockStart_(token)) { + this.startFoldingBlock_(this.SEC_TYPE.CUSTOM, token.start, token.text); + } else if (this.isCustomBlockEnd_(token)) { + // only when there's an outer custom block, treat *endregion; as an end of custom region + if ( + this.searchBlockUpwardOfType_(this.currSection, this.SEC_TYPE.CUSTOM) + ) { + this.tryEndFoldingBlock_( + token.start, + this.SEC_TYPE.CUSTOM, + token.end, + ); + } + } } return token; } @@ -3008,7 +3159,6 @@ export class LexerEx { this.stack[this.stack.length - 2].state === this.PARSING_STATE.IN_MACRO ) { - this.endFoldingBlock_(this.SEC_TYPE.DATA, this.lastToken.end); // end this data section this.stack.pop(); this.stack.push({ parse: this.readMend_, @@ -3022,7 +3172,11 @@ export class LexerEx { case "DATA": //no normal end, and another data meet, there are syntax errors // ignore - this.endFoldingBlock_(this.SEC_TYPE.DATA, this.lastToken.end); + if (this.currSection?.type === this.SEC_TYPE.CUSTOM) { + this.tryPromoteCustomBlock_(); + } else { + this.endFoldingBlock_(this.SEC_TYPE.DATA, this.lastToken.end); + } this.startFoldingBlock_(this.SEC_TYPE.DATA, token.start, word); token.type = Lexer.TOKEN_TYPES.SKEYWORD; @@ -3033,7 +3187,11 @@ export class LexerEx { break; case "PROC": case "PROCEDURE": { - this.endFoldingBlock_(this.SEC_TYPE.DATA, this.lastToken.end); + if (this.currSection?.type === this.SEC_TYPE.CUSTOM) { + this.tryPromoteCustomBlock_(); + } else { + this.endFoldingBlock_(this.SEC_TYPE.DATA, this.lastToken.end); + } this.startFoldingBlock_(this.SEC_TYPE.PROC, token.start, word); token.type = Lexer.TOKEN_TYPES.SKEYWORD; @@ -3052,7 +3210,11 @@ export class LexerEx { break; } case "%MACRO": - this.endFoldingBlock_(this.SEC_TYPE.DATA, this.lastToken.end); + if (this.currSection?.type === this.SEC_TYPE.CUSTOM) { + this.tryPromoteCustomBlock_(); + } else { + this.endFoldingBlock_(this.SEC_TYPE.DATA, this.lastToken.end); + } this.startFoldingBlock_(this.SEC_TYPE.MACRO, token.start, word); token.type = Lexer.TOKEN_TYPES.MSKEYWORD; @@ -3116,9 +3278,83 @@ export class LexerEx { } } } + } else if (Lexer.isComment[token.type]) { + if (this.isCustomBlockStart_(token)) { + this.startFoldingBlock_(this.SEC_TYPE.CUSTOM, token.start, token.text); + } else if (this.isCustomBlockEnd_(token)) { + // only when there's an outer custom block, treat *endregion; as an end of custom region + if ( + this.searchBlockUpwardOfType_(this.currSection, this.SEC_TYPE.CUSTOM) + ) { + this.tryEndFoldingBlock_( + token.start, + this.SEC_TYPE.CUSTOM, + token.end, + ); + } + } } return token; } + + private searchBlockUpwardOfType_( + startBlock: FoldingBlock | undefined, + type: number, + ): FoldingBlock | null { + let cur: FoldingBlock | undefined = startBlock; + while (cur) { + if (cur.type === type) { + return cur; + } else { + cur = cur.outerBlock; + } + } + return null; + } + + private searchLastConsecutiveBlockUpwardOfType_( + startBlock: FoldingBlock | undefined, + ): FoldingBlock | null { + if (!startBlock) { + return null; + } + let cur: FoldingBlock = startBlock; + while (cur.outerBlock && cur.outerBlock.type === cur.type) { + cur = cur.outerBlock; + } + return cur; + } + + private tryPromoteCustomBlock_() { + const curSec = this.currSection; + const outermostCustomBlock: FoldingBlock = + this.searchLastConsecutiveBlockUpwardOfType_(this.currSection)!; + // custom block promotion + if ( + [this.SEC_TYPE.DATA, this.SEC_TYPE.PROC].includes( + outermostCustomBlock?.outerBlock?.type, + ) + ) { + const nearestNonCustomBlock: FoldingBlock | undefined = + outermostCustomBlock.outerBlock; + if (nearestNonCustomBlock) { + const pos = + nearestNonCustomBlock?.innerBlocks.indexOf(outermostCustomBlock); + nearestNonCustomBlock?.innerBlocks.splice(pos, 1); + this.currSection = nearestNonCustomBlock; + this.endFoldingBlock_(nearestNonCustomBlock.type, { + line: outermostCustomBlock.startLine, + column: outermostCustomBlock.startCol, + }); + nearestNonCustomBlock.outerBlock?.innerBlocks.push( + outermostCustomBlock, + ); + outermostCustomBlock.outerBlock = nearestNonCustomBlock.outerBlock; + this.currSection = curSec; + } + } + } + /* * readMacro_ * PROC, DATA %MACRO -----> ignore @@ -3220,6 +3456,21 @@ export class LexerEx { } } } + } else if (Lexer.isComment[token.type]) { + if (this.isCustomBlockStart_(token)) { + this.startFoldingBlock_(this.SEC_TYPE.CUSTOM, token.start, token.text); + } else if (this.isCustomBlockEnd_(token)) { + // only when there's an outer custom block, treat *endregion; as an end of custom region + if ( + this.searchBlockUpwardOfType_(this.currSection, this.SEC_TYPE.CUSTOM) + ) { + this.tryEndFoldingBlock_( + token.start, + this.SEC_TYPE.CUSTOM, + token.end, + ); + } + } } return token; } @@ -3245,7 +3496,9 @@ export class LexerEx { } else { word = token.text; if (word === "PROC" || word === "PROCEDURE") { - this.endFoldingBlock_(this.SEC_TYPE.GBL, this.lastToken.end); + if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + this.endFoldingBlock_(this.SEC_TYPE.GBL, this.lastToken.end); + } this.startFoldingBlock_(this.SEC_TYPE.PROC, token.start, word); token.type = Lexer.TOKEN_TYPES.SKEYWORD; this.stack.pop(); @@ -3261,7 +3514,9 @@ export class LexerEx { name: procName, }); } else if (word === "%MACRO") { - this.endFoldingBlock_(this.SEC_TYPE.GBL, this.lastToken.end); + if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + this.endFoldingBlock_(this.SEC_TYPE.GBL, this.lastToken.end); + } this.startFoldingBlock_(this.SEC_TYPE.MACRO, token.start, word); token.type = Lexer.TOKEN_TYPES.MSKEYWORD; this.stack.pop(); @@ -3274,7 +3529,9 @@ export class LexerEx { state: this.PARSING_STATE.IN_MACRO, }); } else if (word === "DATA") { - this.endFoldingBlock_(this.SEC_TYPE.GBL, this.lastToken.end); + if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + this.endFoldingBlock_(this.SEC_TYPE.GBL, this.lastToken.end); + } this.startFoldingBlock_(this.SEC_TYPE.DATA, token.start, word); token.type = Lexer.TOKEN_TYPES.SKEYWORD; this.stack.pop(); @@ -3289,6 +3546,9 @@ export class LexerEx { } else if (token.type === Lexer.TOKEN_TYPES.MREF) { this.handleMref_(this.PARSING_STATE.IN_GBL); } else { + if (!this.hasFoldingBlock_()) { + this.startFoldingBlock_(this.SEC_TYPE.GBL, token.start, word); + } const validName = this._cleanKeyword(word); const state: any = { parse: this.readGblStmt_, @@ -3308,6 +3568,15 @@ export class LexerEx { } } } + } else if (Lexer.isComment[token.type]) { + if (this.isCustomBlockStart_(token)) { + if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + this.endFoldingBlock_(this.SEC_TYPE.GBL, this.lastToken.end); + } + this.startFoldingBlock_(this.SEC_TYPE.CUSTOM, token.start, token.text); + } else if (this.isCustomBlockEnd_(token)) { + this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); + } } return token; } @@ -3403,16 +3672,15 @@ export class LexerEx { } private readMend_() { const token = this.getNext_(); + const mendToken = this.lastToken; if (token && token.text === ";") { if (this.curr.state === this.PARSING_STATE.IN_MACRO) { this.stack.pop(); this.stack.pop(); - this.endFoldingBlock_( + this.tryEndFoldingBlock_( + mendToken.start, this.SEC_TYPE.MACRO, token.end, - true, - this.curr.start, - this.curr.name, ); } } diff --git a/server/src/sas/SyntaxProvider.ts b/server/src/sas/SyntaxProvider.ts index d226e3142..07a286a2f 100644 --- a/server/src/sas/SyntaxProvider.ts +++ b/server/src/sas/SyntaxProvider.ts @@ -361,8 +361,16 @@ export class SyntaxProvider { line: number, col?: number, strict?: boolean, + ignoreCustomBlock?: boolean, + ignoreGlobalBlock?: boolean, ): FoldingBlock | null { - return this.lexer.getFoldingBlock(line, col, strict); + return this.lexer.getFoldingBlock( + line, + col, + strict, + ignoreCustomBlock, + ignoreGlobalBlock, + ); } add(change: Change): void { this._push(change); diff --git a/server/src/server.ts b/server/src/server.ts index 11e33009d..53e88c59d 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -117,22 +117,28 @@ export const init = (conn: Connection): void => { connection.onDocumentSymbol((params) => { const languageService = getLanguageService(params.textDocument.uri); - return languageService - .getFoldingBlocks() - .filter((symbol) => symbol.name !== "custom"); + return languageService.getDocumentSymbols(); }); connection.onFoldingRanges((params) => { const languageService = getLanguageService(params.textDocument.uri); - return languageService.getFoldingBlocks().map((block) => ({ - startLine: block.range.start.line, - endLine: block.range.end.line, - })); + return languageService.getFoldingRanges(); }); connection.onRequest("sas/getFoldingBlock", (params) => { const languageService = getLanguageService(params.textDocument.uri); - return languageService.getFoldingBlock(params.line, params.col); + const block = languageService.getFoldingBlock( + params.line, + params.col, + true, + false, + false, + ); + if (!block) { + return undefined; + } else { + return { ...block, outerBlock: undefined, innerBlocks: undefined }; + } }); connection.onDocumentOnTypeFormatting((params) => {