From 2a9016000fba72955be3c44050ca4c617b93f6e4 Mon Sep 17 00:00:00 2001 From: Jonas <932166095@qq.com> Date: Sun, 1 Oct 2023 23:49:32 +0800 Subject: [PATCH 01/17] feat: recursive folding block --- server/src/sas/LanguageServiceProvider.ts | 79 +++--- server/src/sas/LexerEx.ts | 291 ++++++++++++++++------ 2 files changed, 242 insertions(+), 128 deletions(-) diff --git a/server/src/sas/LanguageServiceProvider.ts b/server/src/sas/LanguageServiceProvider.ts index 5c22b135b..1dc930aa9 100644 --- a/server/src/sas/LanguageServiceProvider.ts +++ b/server/src/sas/LanguageServiceProvider.ts @@ -1,10 +1,11 @@ // Copyright © 2022, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { TextDocument } from "vscode-languageserver-textdocument"; +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"; @@ -120,61 +121,49 @@ export class LanguageServiceProvider { getFoldingBlocks(): 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; - 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), + const rootBlock = this.syntaxProvider.getFoldingBlock(i); + if (rootBlock && rootBlock.startLine === i) { + const flattenBlocks = this._flattenFoldingBlockTree(rootBlock); + for (const block of flattenBlocks) { + const range: Range = { + start: { line: block.startLine, character: block.startCol }, + end: { line: block.endFoldingLine, character: block.endFoldingCol }, }; result.push({ - name: "custom", - kind: SymbolKind.Module, - range: customBlock, - selectionRange: customBlock, + name: + block.type === 1 + ? this._getProcName(block.startLine) + : block.name, + kind: SymbolKinds[block.type], + range, + selectionRange: range, }); - customBlock = undefined; } + i = rootBlock.endFoldingLine; + continue; } } return result; } + // 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) { return this.syntaxProvider.getFoldingBlock(line, col, true); } diff --git a/server/src/sas/LexerEx.ts b/server/src/sas/LexerEx.ts index 04365818c..37a784292 100644 --- a/server/src/sas/LexerEx.ts +++ b/server/src/sas/LexerEx.ts @@ -13,6 +13,8 @@ import { TextPosition, arrayToMap } from "./utils"; */ export class FoldingBlock { + outerBlock?: FoldingBlock; + innerBlocks: FoldingBlock[]; startLine: number; startCol: number; endLine: number; @@ -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); @@ -589,20 +595,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 +637,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 +659,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,54 +704,70 @@ 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) { + if (blocks[idx] && blocks[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) { 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 { @@ -732,16 +776,16 @@ export class LexerEx { } return this.sectionCache[line]; } - return this.getFoldingBlock_(line, col, strict); + return this.getFoldingBlock_(this.sections, line, col, strict); } 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 +822,8 @@ export class LexerEx { } private resetFoldingBlockCache_() { this.sections = []; - this.blockDepth = 0; } - private tryEndFoldingBlock_(pos: TextPosition) { + private tryEndFoldingBlock_(pos: TextPosition, untilType?: number) { if (this.hasFoldingBlock_()) { // handle text end let secType = this.SEC_TYPE.PROC; @@ -791,11 +834,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) { + let stop = false; + while (this.currSection && !stop) { + if (untilType && this.currSection.type === untilType) { + stop = true; + } this.endFoldingBlock_(secType, pos); - this.blockDepth--; } } } @@ -810,7 +854,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 +1207,7 @@ export class LexerEx { } } } + private start_(change: Change) { const parseRange = this.getParseRangeBySections_(change); @@ -1173,7 +1218,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 +1226,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 +1464,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 +1660,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 +1738,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 { + this.startFoldingBlock_(this.SEC_TYPE.GBL, token.start, word); + } this.stack.push({ parse: this.readGbl_, state: this.PARSING_STATE.IN_GBL, @@ -2463,7 +2540,9 @@ 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.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 +2560,9 @@ export class LexerEx { break; } case "DATA": - this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + 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 +2576,9 @@ export class LexerEx { }); break; case "%MACRO": - this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + 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); @@ -2826,7 +2909,9 @@ 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.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 +2930,9 @@ export class LexerEx { break; } case "%MACRO": - this.endFoldingBlock_(this.SEC_TYPE.PROC, this.lastToken.end); + if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + 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 +2948,9 @@ 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.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 +3034,12 @@ 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)) { + this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); + } } return token; } @@ -3022,7 +3117,9 @@ 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.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 +3130,9 @@ 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.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 +3151,9 @@ export class LexerEx { break; } case "%MACRO": - this.endFoldingBlock_(this.SEC_TYPE.DATA, this.lastToken.end); + if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + 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,6 +3217,12 @@ 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)) { + this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); + } } return token; } @@ -3220,6 +3327,12 @@ 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)) { + this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); + } } return token; } @@ -3245,7 +3358,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 +3376,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 +3391,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(); @@ -3308,6 +3427,12 @@ 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)) { + this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); + } } return token; } From 33a9b264434f5a987aef64b06a3d5b7a798cd4fe Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Wed, 11 Oct 2023 17:25:43 +0800 Subject: [PATCH 02/17] fix: auto complete in custom region --- server/src/sas/CodeZoneManager.ts | 9 ++++++++- server/src/sas/FormatOnTypeProvider.ts | 2 +- server/src/sas/LanguageServiceProvider.ts | 14 ++++++++++++-- server/src/sas/LexerEx.ts | 19 ++++++++++++++++--- server/src/sas/SyntaxProvider.ts | 3 ++- server/src/server.ts | 7 ++++++- 6 files changed, 45 insertions(+), 9 deletions(-) diff --git a/server/src/sas/CodeZoneManager.ts b/server/src/sas/CodeZoneManager.ts index 631f5c478..976ffd28b 100644 --- a/server/src/sas/CodeZoneManager.ts +++ b/server/src/sas/CodeZoneManager.ts @@ -808,6 +808,8 @@ export class CodeZoneManager { block = this._syntaxProvider.getFoldingBlock( tmpContext.line, tmpContext.col, + false, + true, ); if (block) { if (this._inBlock(block, token)! < 0 && !this._endedReally(block)) { @@ -2428,7 +2430,12 @@ 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, + ); /* 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..24be783da 100644 --- a/server/src/sas/FormatOnTypeProvider.ts +++ b/server/src/sas/FormatOnTypeProvider.ts @@ -95,7 +95,7 @@ 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); 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 1dc930aa9..53338bc79 100644 --- a/server/src/sas/LanguageServiceProvider.ts +++ b/server/src/sas/LanguageServiceProvider.ts @@ -164,8 +164,18 @@ export class LanguageServiceProvider { return resultList; } - getFoldingBlock(line: number, col: number) { - return this.syntaxProvider.getFoldingBlock(line, col, true); + getFoldingBlock( + line: number, + col: number, + strict?: boolean, + ignoreCustomBlock?: boolean, + ) { + return this.syntaxProvider.getFoldingBlock( + line, + col, + strict, + ignoreCustomBlock, + ); } setLibService(fn: LibService): void { diff --git a/server/src/sas/LexerEx.ts b/server/src/sas/LexerEx.ts index 37a784292..da76b8a32 100644 --- a/server/src/sas/LexerEx.ts +++ b/server/src/sas/LexerEx.ts @@ -764,7 +764,13 @@ export class LexerEx { return null; } } - getFoldingBlock(line: number, col?: number, strict?: boolean) { + getFoldingBlock( + line: number, + col?: number, + strict?: boolean, + ignoreCustomBlock?: boolean, + ): FoldingBlock | null { + let block: FoldingBlock | null = null; if (col === undefined) { if (!this.sectionCache[line]) { const section = this.getFoldingBlock_(this.sections, line); @@ -774,9 +780,16 @@ export class LexerEx { this.sectionCache[line] = null; } } - return this.sectionCache[line]; + block = this.sectionCache[line]; + } else { + block = this.getFoldingBlock_(this.sections, line, col, strict); + } + if (block && ignoreCustomBlock) { + while (block?.type === this.SEC_TYPE.CUSTOM) { + block = block.outerBlock ?? null; + } } - return this.getFoldingBlock_(this.sections, line, col, strict); + return block; } private getBlockPos_(blocks: FoldingBlock[], line: number, col?: number) { let idx = this.getBlockPos1_(blocks, line); diff --git a/server/src/sas/SyntaxProvider.ts b/server/src/sas/SyntaxProvider.ts index d226e3142..d67a8f0e4 100644 --- a/server/src/sas/SyntaxProvider.ts +++ b/server/src/sas/SyntaxProvider.ts @@ -361,8 +361,9 @@ export class SyntaxProvider { line: number, col?: number, strict?: boolean, + ignoreCustomBlock?: boolean, ): FoldingBlock | null { - return this.lexer.getFoldingBlock(line, col, strict); + return this.lexer.getFoldingBlock(line, col, strict, ignoreCustomBlock); } add(change: Change): void { this._push(change); diff --git a/server/src/server.ts b/server/src/server.ts index 12ef4c124..76a017936 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -129,7 +129,12 @@ export const init = (conn: Connection): void => { connection.onRequest("sas/getFoldingBlock", (params) => { const languageService = getLanguageService(params.textDocument.uri); - return languageService.getFoldingBlock(params.line, params.col); + return languageService.getFoldingBlock( + params.line, + params.col, + true, + false, + ); }); connection.onDocumentOnTypeFormatting((params) => { From 4de9f259f4ccb2f8d6845225f6adea00818bd9b4 Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Tue, 17 Oct 2023 19:53:19 +0800 Subject: [PATCH 03/17] fix: outline bug of rec-block --- server/src/sas/LanguageServiceProvider.ts | 9 +++++++-- server/src/server.ts | 6 ++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/server/src/sas/LanguageServiceProvider.ts b/server/src/sas/LanguageServiceProvider.ts index 53338bc79..6c3e34981 100644 --- a/server/src/sas/LanguageServiceProvider.ts +++ b/server/src/sas/LanguageServiceProvider.ts @@ -119,12 +119,17 @@ export class LanguageServiceProvider { return data; } - getFoldingBlocks(): DocumentSymbol[] { + getDocumentSymbols(): DocumentSymbol[] { const lineCount = this.model.getLineCount(); const result: DocumentSymbol[] = []; for (let i = 0; i < lineCount; i++) { - const rootBlock = this.syntaxProvider.getFoldingBlock(i); + const rootBlock = this.syntaxProvider.getFoldingBlock( + i, + undefined, + false, + true, + ); if (rootBlock && rootBlock.startLine === i) { const flattenBlocks = this._flattenFoldingBlockTree(rootBlock); for (const block of flattenBlocks) { diff --git a/server/src/server.ts b/server/src/server.ts index 76a017936..ee9870a8f 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -114,14 +114,12 @@ 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) => ({ + return languageService.getDocumentSymbols().map((block) => ({ startLine: block.range.start.line, endLine: block.range.end.line, })); From 9e67754f24d57143411ef79a26d080a0ab99e925 Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Wed, 18 Oct 2023 22:50:24 +0800 Subject: [PATCH 04/17] fix: show custom region in outline --- server/src/sas/LanguageServiceProvider.ts | 75 ++++++++++++++++------- server/src/server.ts | 5 +- 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/server/src/sas/LanguageServiceProvider.ts b/server/src/sas/LanguageServiceProvider.ts index 6c3e34981..b550f6a17 100644 --- a/server/src/sas/LanguageServiceProvider.ts +++ b/server/src/sas/LanguageServiceProvider.ts @@ -1,5 +1,6 @@ // Copyright © 2022, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { FoldingRange } from "vscode-languageserver"; import { Range, TextDocument } from "vscode-languageserver-textdocument"; import { DocumentSymbol, SymbolKind } from "vscode-languageserver-types"; @@ -39,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; @@ -124,27 +131,53 @@ export class LanguageServiceProvider { const result: DocumentSymbol[] = []; for (let i = 0; i < lineCount; i++) { - const rootBlock = this.syntaxProvider.getFoldingBlock( - i, - undefined, - false, - true, - ); + const rootBlock = this.syntaxProvider.getFoldingBlock(i); if (rootBlock && rootBlock.startLine === i) { - const flattenBlocks = this._flattenFoldingBlockTree(rootBlock); - for (const block of flattenBlocks) { - const range: Range = { - start: { line: block.startLine, character: block.startCol }, - end: { line: block.endFoldingLine, character: block.endFoldingCol }, - }; + const docSymbol: DocumentSymbol = this._buildDocumentSymbol(rootBlock); + result.push(docSymbol); + i = rootBlock.endFoldingLine; + continue; + } + } + 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); + if (rootBlock && rootBlock.startLine === i) { + const blocks: FoldingBlock[] = this._flattenFoldingBlockTree(rootBlock); + for (const block of blocks) { result.push({ - name: - block.type === 1 - ? this._getProcName(block.startLine) - : block.name, - kind: SymbolKinds[block.type], - range, - selectionRange: range, + startLine: block.startLine, + endLine: block.endFoldingLine, }); } i = rootBlock.endFoldingLine; diff --git a/server/src/server.ts b/server/src/server.ts index ee9870a8f..e5a0cb14e 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -119,10 +119,7 @@ export const init = (conn: Connection): void => { connection.onFoldingRanges((params) => { const languageService = getLanguageService(params.textDocument.uri); - return languageService.getDocumentSymbols().map((block) => ({ - startLine: block.range.start.line, - endLine: block.range.end.line, - })); + return languageService.getFoldingRanges(); }); connection.onRequest("sas/getFoldingBlock", (params) => { From 4e483a65cbc817192049e9c666d3e6b4ef946083 Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Fri, 20 Oct 2023 15:06:14 +0800 Subject: [PATCH 05/17] fix: run region fail --- server/src/sas/LexerEx.ts | 4 ++-- server/src/server.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/sas/LexerEx.ts b/server/src/sas/LexerEx.ts index da76b8a32..81658ddc3 100644 --- a/server/src/sas/LexerEx.ts +++ b/server/src/sas/LexerEx.ts @@ -13,8 +13,6 @@ import { TextPosition, arrayToMap } from "./utils"; */ export class FoldingBlock { - outerBlock?: FoldingBlock; - innerBlocks: FoldingBlock[]; startLine: number; startCol: number; endLine: number; @@ -29,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 diff --git a/server/src/server.ts b/server/src/server.ts index e5a0cb14e..5cb68a1d3 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -124,12 +124,13 @@ export const init = (conn: Connection): void => { connection.onRequest("sas/getFoldingBlock", (params) => { const languageService = getLanguageService(params.textDocument.uri); - return languageService.getFoldingBlock( + const block = languageService.getFoldingBlock( params.line, params.col, true, false, ); + return { ...block, outerBlock: undefined, innerBlocks: undefined }; }); connection.onDocumentOnTypeFormatting((params) => { From 67f902716af2e49ef965b3668ed9c2e3e8222b93 Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Tue, 7 Nov 2023 15:00:52 +0800 Subject: [PATCH 06/17] fix: rec block issues --- server/src/sas/LexerEx.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/server/src/sas/LexerEx.ts b/server/src/sas/LexerEx.ts index 81658ddc3..2c50dc495 100644 --- a/server/src/sas/LexerEx.ts +++ b/server/src/sas/LexerEx.ts @@ -222,7 +222,7 @@ export class LexerEx { block = blocks[i]; } while (block && !this._isBefore(this._endPos(block), pos)); // []| - return ++i; + return block ? ++i : -1; } else { /* []| -> @@ -231,7 +231,7 @@ export class LexerEx { i++; block = blocks[i]; } while (block && !this._isBefore(pos, this._startPos(block))); // |[] - return --i; + return block ? --i : -1; } } @@ -719,10 +719,6 @@ export class LexerEx { block = blocks[i]; } } - // ignore global - if (blocks[idx] && blocks[idx].type === this.SEC_TYPE.GBL) { - idx = -1; - } return blocks[idx] ?? null; //return sections[idx]?sections[idx]:null; //we return null if no } @@ -758,7 +754,12 @@ export class LexerEx { this.getFoldingBlock_(block!.innerBlocks, line, col, strict) ?? block ); } else { - return block; + // ignore global + if (block?.type === this.SEC_TYPE.GBL) { + return null; + } else { + return block; + } } } else { return null; From d525e82ad894fe5d941d7555dd0e72e3d8f67592 Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Mon, 13 Nov 2023 15:02:17 +0800 Subject: [PATCH 07/17] fix: close block when new gbl block starts --- server/src/sas/LexerEx.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/server/src/sas/LexerEx.ts b/server/src/sas/LexerEx.ts index 2c50dc495..0766ac8e8 100644 --- a/server/src/sas/LexerEx.ts +++ b/server/src/sas/LexerEx.ts @@ -208,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; } @@ -389,18 +389,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( @@ -1763,6 +1775,7 @@ export class LexerEx { this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); } } else { + this.tryEndFoldingBlock_(token.end); this.startFoldingBlock_(this.SEC_TYPE.GBL, token.start, word); } this.stack.push({ From 79c87d10fbab2c2e34211dee445a38d9b7bc05e9 Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Wed, 15 Nov 2023 11:26:12 +0800 Subject: [PATCH 08/17] fix: rec block match issue --- client/src/commands/run.ts | 10 +++++++++- server/src/sas/LexerEx.ts | 26 ++++++++++++++++++++------ server/src/server.ts | 6 +++++- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/client/src/commands/run.ts b/client/src/commands/run.ts index 3df354883..480d2bfa5 100644 --- a/client/src/commands/run.ts +++ b/client/src/commands/run.ts @@ -191,7 +191,15 @@ export async function runSelected(uri: Uri): Promise { export async function runRegion(client: BaseLanguageClient): Promise { const selections = await getSelectedRegions(client); - window.activeTextEditor.selections = selections; + const editor = window.activeTextEditor; + const doc = editor.document; + if (selections.length === 0) { + editor.selections = [ + new Selection(doc.positionAt(0), doc.positionAt(doc.getText().length)), + ]; + } else { + editor.selections = selections; + } await _run(true); } diff --git a/server/src/sas/LexerEx.ts b/server/src/sas/LexerEx.ts index 0766ac8e8..ace9e00df 100644 --- a/server/src/sas/LexerEx.ts +++ b/server/src/sas/LexerEx.ts @@ -220,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 block ? ++i : -1; + return block ? i : -1; } else { /* []| -> @@ -230,8 +233,11 @@ export class LexerEx { do { i++; block = blocks[i]; - } while (block && !this._isBefore(pos, this._startPos(block))); // |[] - return block ? --i : -1; + } while ( + block && + !this._isBetween(pos, this._startPos(block), this._endPos(block), true) + ); // |[] + return block ? i : -1; } } @@ -1774,8 +1780,7 @@ export class LexerEx { } else if (this.isCustomBlockEnd_(token)) { this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); } - } else { - this.tryEndFoldingBlock_(token.end); + } else if (!this.hasFoldingBlock_()) { this.startFoldingBlock_(this.SEC_TYPE.GBL, token.start, word); } this.stack.push({ @@ -3065,6 +3070,9 @@ export class LexerEx { if (this.isCustomBlockStart_(token)) { this.startFoldingBlock_(this.SEC_TYPE.CUSTOM, token.start, token.text); } else if (this.isCustomBlockEnd_(token)) { + if (this.currSection?.type === this.SEC_TYPE.PROC) { + this.endFoldingBlock_(this.SEC_TYPE.PROC, token.start); + } this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); } } @@ -3248,6 +3256,9 @@ export class LexerEx { if (this.isCustomBlockStart_(token)) { this.startFoldingBlock_(this.SEC_TYPE.CUSTOM, token.start, token.text); } else if (this.isCustomBlockEnd_(token)) { + if (this.currSection?.type === this.SEC_TYPE.DATA) { + this.endFoldingBlock_(this.SEC_TYPE.DATA, token.start); + } this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); } } @@ -3358,6 +3369,9 @@ export class LexerEx { if (this.isCustomBlockStart_(token)) { this.startFoldingBlock_(this.SEC_TYPE.CUSTOM, token.start, token.text); } else if (this.isCustomBlockEnd_(token)) { + if (this.currSection?.type === this.SEC_TYPE.MACRO) { + this.endFoldingBlock_(this.SEC_TYPE.MACRO, token.start); + } this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); } } diff --git a/server/src/server.ts b/server/src/server.ts index 5cb68a1d3..3cf937de0 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -130,7 +130,11 @@ export const init = (conn: Connection): void => { true, false, ); - return { ...block, outerBlock: undefined, innerBlocks: undefined }; + if (!block) { + return undefined; + } else { + return { ...block, outerBlock: undefined, innerBlocks: undefined }; + } }); connection.onDocumentOnTypeFormatting((params) => { From 1955924e7c6e25e98ecb6fec62ac1f305ecac1d6 Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Fri, 17 Nov 2023 14:00:14 +0800 Subject: [PATCH 09/17] fix: promote custom block when unembeddable --- server/src/sas/LexerEx.ts | 128 ++++++++++++++++++++++++++++++++------ 1 file changed, 110 insertions(+), 18 deletions(-) diff --git a/server/src/sas/LexerEx.ts b/server/src/sas/LexerEx.ts index ace9e00df..4f160eed8 100644 --- a/server/src/sas/LexerEx.ts +++ b/server/src/sas/LexerEx.ts @@ -2572,7 +2572,9 @@ export class LexerEx { break; case "PROC": case "PROCEDURE": { - if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + 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); @@ -2592,7 +2594,9 @@ export class LexerEx { break; } case "DATA": - if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + 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); @@ -2608,7 +2612,9 @@ export class LexerEx { }); break; case "%MACRO": - if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + 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); @@ -2941,7 +2947,9 @@ export class LexerEx { case "PROCEDURE": { //no normal end, and another proc meet, there are syntax errors // ignore - if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + 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); @@ -2962,7 +2970,9 @@ export class LexerEx { break; } case "%MACRO": - if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + 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); @@ -2980,7 +2990,9 @@ export class LexerEx { break; case "DATA": if (!this.DS2_[procName]) { - if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + 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); @@ -3070,10 +3082,15 @@ export class LexerEx { if (this.isCustomBlockStart_(token)) { this.startFoldingBlock_(this.SEC_TYPE.CUSTOM, token.start, token.text); } else if (this.isCustomBlockEnd_(token)) { - if (this.currSection?.type === this.SEC_TYPE.PROC) { - this.endFoldingBlock_(this.SEC_TYPE.PROC, token.start); + // 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) + ) { + if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + this.endFoldingBlock_(this.SEC_TYPE.DATA, token.start); + } + this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); } - this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); } } return token; @@ -3152,7 +3169,9 @@ export class LexerEx { case "DATA": //no normal end, and another data meet, there are syntax errors // ignore - if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + 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); @@ -3165,7 +3184,9 @@ export class LexerEx { break; case "PROC": case "PROCEDURE": { - if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + 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); @@ -3186,7 +3207,9 @@ export class LexerEx { break; } case "%MACRO": - if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + 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); @@ -3256,14 +3279,78 @@ export class LexerEx { if (this.isCustomBlockStart_(token)) { this.startFoldingBlock_(this.SEC_TYPE.CUSTOM, token.start, token.text); } else if (this.isCustomBlockEnd_(token)) { - if (this.currSection?.type === this.SEC_TYPE.DATA) { - this.endFoldingBlock_(this.SEC_TYPE.DATA, token.start); + // 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) + ) { + if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + this.endFoldingBlock_(this.SEC_TYPE.DATA, token.start); + } + this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); } - this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); } } 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 @@ -3369,10 +3456,15 @@ export class LexerEx { if (this.isCustomBlockStart_(token)) { this.startFoldingBlock_(this.SEC_TYPE.CUSTOM, token.start, token.text); } else if (this.isCustomBlockEnd_(token)) { - if (this.currSection?.type === this.SEC_TYPE.MACRO) { - this.endFoldingBlock_(this.SEC_TYPE.MACRO, token.start); + // 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) + ) { + if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { + this.endFoldingBlock_(this.SEC_TYPE.DATA, token.start); + } + this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); } - this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); } } return token; From 284baea4335ef9a7ff9e7b664bcd534de62954e6 Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Tue, 21 Nov 2023 17:19:49 +0800 Subject: [PATCH 10/17] fix: disable run region when only select open code --- client/src/commands/run.ts | 61 +----------------------------------- client/src/node/extension.ts | 18 +++++++++++ client/src/utils/utils.ts | 60 +++++++++++++++++++++++++++++++++++ server/src/sas/LexerEx.ts | 3 ++ 4 files changed, 82 insertions(+), 60 deletions(-) create mode 100644 client/src/utils/utils.ts diff --git a/client/src/commands/run.ts b/client/src/commands/run.ts index 480d2bfa5..e3eb1c33c 100644 --- a/client/src/commands/run.ts +++ b/client/src/commands/run.ts @@ -2,9 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { EventEmitter, - Position, ProgressLocation, - Selection, Uri, commands, l10n, @@ -20,15 +18,9 @@ import { isOutputHtmlEnabled } from "../components/Helper/SettingHelper"; import { LogFn as LogChannelFn } from "../components/LogChannel"; import { showResult } from "../components/ResultPanel"; import { OnLogFn, RunResult, getSession } from "../connection"; +import { getSelectedRegions } from "../utils/utils"; import { profileConfig, switchProfile } from "./profile"; -interface FoldingBlock { - startLine: number; - startCol: number; - endLine: number; - endCol: number; -} - let running = false; function getCode(selected = false, uri?: Uri): string { @@ -71,57 +63,6 @@ function getCode(selected = false, uri?: Uri): string { return wrapCodeWithOutputHtml(code); } -async function getSelectedRegions( - client: BaseLanguageClient, -): Promise { - const result: string[] = []; - - async function pushBlock(line: number, col: number) { - const block = await client.sendRequest( - "sas/getFoldingBlock", - { - textDocument: { uri: window.activeTextEditor.document.uri.toString() }, - line, - col, - }, - ); - if (block) { - const start = doc.offsetAt(new Position(block.startLine, block.startCol)); - const end = doc.offsetAt(new Position(block.endLine, block.endCol)); - const key = `${start}-${end}`; - if (result.indexOf(key) === -1) { - result.push(key); - } - return end; - } - } - - const editor = window.activeTextEditor; - const doc = editor.document; - for (const selection of editor.selections) { - const start = doc.offsetAt(selection.start); - let end = doc.offsetAt(selection.end); - const selectedText = doc.getText(selection); - if (selectedText.endsWith("\n")) { - --end; - } - for (let i = start; i <= end; i++) { - const pos = doc.positionAt(i); - const blockEnd = await pushBlock(pos.line, pos.character); - if (blockEnd && blockEnd > i) { - i = blockEnd; - } - } - } - return result.map((key) => { - const [start, end] = key.split("-"); - return new Selection( - doc.positionAt(parseInt(start)), - doc.positionAt(parseInt(end)), - ); - }); -} - async function runCode(selected?: boolean, uri?: Uri) { if (profileConfig.getActiveProfile() === "") { switchProfile(); diff --git a/client/src/node/extension.ts b/client/src/node/extension.ts index 924ae9713..7ed2e4454 100644 --- a/client/src/node/extension.ts +++ b/client/src/node/extension.ts @@ -8,6 +8,7 @@ import { NotebookData, StatusBarAlignment, StatusBarItem, + TextEditorSelectionChangeEvent, Uri, authentication, commands, @@ -47,6 +48,7 @@ import { NotebookSerializer } from "../components/notebook/Serializer"; import { ConnectionType } from "../components/profile"; import { SasTaskProvider } from "../components/tasks/SasTaskProvider"; import { SAS_TASK_TYPE } from "../components/tasks/SasTasks"; +import { getSelectedRegions } from "../utils/utils"; let client: LanguageClient; // Create Profile status bar item @@ -55,6 +57,13 @@ const activeProfileStatusBarIcon = window.createStatusBarItem( 0, ); +interface FoldingBlock { + startLine: number; + startCol: number; + endLine: number; + endCol: number; +} + export function activate(context: ExtensionContext): void { // The server is implemented in node const serverModule = context.asAbsolutePath( @@ -129,6 +138,7 @@ export function activate(context: ExtensionContext): void { "SAS", new SASAuthProvider(context.secrets), ), + languages.registerDocumentSemanticTokensProvider( { language: "sas-log" }, LogTokensProvider, @@ -147,6 +157,14 @@ export function activate(context: ExtensionContext): void { "sas-notebook", new NotebookSerializer(), ), + window.onDidChangeTextEditorSelection(async () => { + const selections = await getSelectedRegions(client); + if (selections.length === 0) { + commands.executeCommand("setContext", "SAS.running", true); + } else { + commands.executeCommand("setContext", "SAS.running", false); + } + }), new NotebookController(), commands.registerCommand("SAS.notebook.new", async () => { await window.showNotebookDocument( diff --git a/client/src/utils/utils.ts b/client/src/utils/utils.ts new file mode 100644 index 000000000..730f913de --- /dev/null +++ b/client/src/utils/utils.ts @@ -0,0 +1,60 @@ +import { Position, Selection, window } from "vscode"; +import { BaseLanguageClient } from "vscode-languageclient"; + +interface FoldingBlock { + startLine: number; + startCol: number; + endLine: number; + endCol: number; +} + +export async function getSelectedRegions( + client: BaseLanguageClient, +): Promise { + const result: string[] = []; + + async function pushBlock(line: number, col: number) { + const block = await client.sendRequest( + "sas/getFoldingBlock", + { + textDocument: { uri: window.activeTextEditor.document.uri.toString() }, + line, + col, + }, + ); + if (block) { + const start = doc.offsetAt(new Position(block.startLine, block.startCol)); + const end = doc.offsetAt(new Position(block.endLine, block.endCol)); + const key = `${start}-${end}`; + if (result.indexOf(key) === -1) { + result.push(key); + } + return end; + } + } + + const editor = window.activeTextEditor; + const doc = editor.document; + for (const selection of editor.selections) { + const start = doc.offsetAt(selection.start); + let end = doc.offsetAt(selection.end); + const selectedText = doc.getText(selection); + if (selectedText.endsWith("\n")) { + --end; + } + for (let i = start; i <= end; i++) { + const pos = doc.positionAt(i); + const blockEnd = await pushBlock(pos.line, pos.character); + if (blockEnd && blockEnd > i) { + i = blockEnd; + } + } + } + return result.map((key) => { + const [start, end] = key.split("-"); + return new Selection( + doc.positionAt(parseInt(start)), + doc.positionAt(parseInt(end)), + ); + }); +} diff --git a/server/src/sas/LexerEx.ts b/server/src/sas/LexerEx.ts index 4f160eed8..ab99d4981 100644 --- a/server/src/sas/LexerEx.ts +++ b/server/src/sas/LexerEx.ts @@ -3562,6 +3562,9 @@ 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); From cd2fa4d3e1e08d0c78d8e7358627bb36ce8161f8 Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Wed, 22 Nov 2023 15:00:18 +0800 Subject: [PATCH 11/17] fix: run region for open code --- client/src/node/extension.ts | 8 -------- server/src/sas/CodeZoneManager.ts | 2 ++ server/src/sas/FormatOnTypeProvider.ts | 8 +++++++- server/src/sas/LanguageServiceProvider.ts | 18 ++++++++++++++++-- server/src/sas/LexerEx.ts | 17 +++++++---------- server/src/sas/SyntaxProvider.ts | 9 ++++++++- server/src/server.ts | 1 + 7 files changed, 41 insertions(+), 22 deletions(-) diff --git a/client/src/node/extension.ts b/client/src/node/extension.ts index 5d4c022bf..3d176ca3d 100644 --- a/client/src/node/extension.ts +++ b/client/src/node/extension.ts @@ -152,14 +152,6 @@ export function activate(context: ExtensionContext): void { "sas-notebook", new NotebookSerializer(), ), - window.onDidChangeTextEditorSelection(async () => { - const selections = await getSelectedRegions(client); - if (selections.length === 0) { - commands.executeCommand("setContext", "SAS.running", true); - } else { - commands.executeCommand("setContext", "SAS.running", false); - } - }), new NotebookController(), commands.registerCommand("SAS.notebook.new", async () => { await window.showNotebookDocument( diff --git a/server/src/sas/CodeZoneManager.ts b/server/src/sas/CodeZoneManager.ts index 976ffd28b..733d704b0 100644 --- a/server/src/sas/CodeZoneManager.ts +++ b/server/src/sas/CodeZoneManager.ts @@ -810,6 +810,7 @@ export class CodeZoneManager { tmpContext.col, false, true, + true, ); if (block) { if (this._inBlock(block, token)! < 0 && !this._endedReally(block)) { @@ -2435,6 +2436,7 @@ export class CodeZoneManager { tmpCol, false, true, + true, ); /* first check type to determine zone, some special conditions * 1) for bringing up auto completion popup by shortcut, diff --git a/server/src/sas/FormatOnTypeProvider.ts b/server/src/sas/FormatOnTypeProvider.ts index 24be783da..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, false, true); + 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 b550f6a17..e50f96d11 100644 --- a/server/src/sas/LanguageServiceProvider.ts +++ b/server/src/sas/LanguageServiceProvider.ts @@ -131,7 +131,13 @@ export class LanguageServiceProvider { const result: DocumentSymbol[] = []; for (let i = 0; i < lineCount; i++) { - const rootBlock = this.syntaxProvider.getFoldingBlock(i); + const rootBlock = this.syntaxProvider.getFoldingBlock( + i, + undefined, + false, + false, + true, + ); if (rootBlock && rootBlock.startLine === i) { const docSymbol: DocumentSymbol = this._buildDocumentSymbol(rootBlock); result.push(docSymbol); @@ -171,7 +177,13 @@ export class LanguageServiceProvider { const result: FoldingRange[] = []; for (let i = 0; i < lineCount; i++) { - const rootBlock = this.syntaxProvider.getFoldingBlock(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) { @@ -207,12 +219,14 @@ export class LanguageServiceProvider { col: number, strict?: boolean, ignoreCustomBlock?: boolean, + ignoreGlobalBlock?: boolean, ) { return this.syntaxProvider.getFoldingBlock( line, col, strict, ignoreCustomBlock, + ignoreGlobalBlock, ); } diff --git a/server/src/sas/LexerEx.ts b/server/src/sas/LexerEx.ts index ab99d4981..278f3eff6 100644 --- a/server/src/sas/LexerEx.ts +++ b/server/src/sas/LexerEx.ts @@ -772,12 +772,7 @@ export class LexerEx { this.getFoldingBlock_(block!.innerBlocks, line, col, strict) ?? block ); } else { - // ignore global - if (block?.type === this.SEC_TYPE.GBL) { - return null; - } else { - return block; - } + return block; } } else { return null; @@ -788,6 +783,7 @@ export class LexerEx { col?: number, strict?: boolean, ignoreCustomBlock?: boolean, + ignoreGlobalBlock?: boolean, ): FoldingBlock | null { let block: FoldingBlock | null = null; if (col === undefined) { @@ -803,10 +799,11 @@ export class LexerEx { } else { block = this.getFoldingBlock_(this.sections, line, col, strict); } - if (block && ignoreCustomBlock) { - while (block?.type === this.SEC_TYPE.CUSTOM) { - block = block.outerBlock ?? null; - } + while ( + (ignoreCustomBlock && block?.type === this.SEC_TYPE.CUSTOM) || + (ignoreGlobalBlock && block?.type === this.SEC_TYPE.GBL) + ) { + block = block.outerBlock ?? null; } return block; } diff --git a/server/src/sas/SyntaxProvider.ts b/server/src/sas/SyntaxProvider.ts index d67a8f0e4..07a286a2f 100644 --- a/server/src/sas/SyntaxProvider.ts +++ b/server/src/sas/SyntaxProvider.ts @@ -362,8 +362,15 @@ export class SyntaxProvider { col?: number, strict?: boolean, ignoreCustomBlock?: boolean, + ignoreGlobalBlock?: boolean, ): FoldingBlock | null { - return this.lexer.getFoldingBlock(line, col, strict, ignoreCustomBlock); + 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 3cf937de0..62737a8dc 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -129,6 +129,7 @@ export const init = (conn: Connection): void => { params.col, true, false, + false, ); if (!block) { return undefined; From 20dc5bf7c4509d49f0e2db02b12da9e61180465d Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Thu, 23 Nov 2023 17:02:57 +0800 Subject: [PATCH 12/17] fix: mend after custom region in data --- client/src/commands/run.ts | 58 +++++++++++++++++++++++++++++++++++- client/src/utils/utils.ts | 60 -------------------------------------- server/src/sas/LexerEx.ts | 47 ++++++++++++++++------------- 3 files changed, 84 insertions(+), 81 deletions(-) delete mode 100644 client/src/utils/utils.ts diff --git a/client/src/commands/run.ts b/client/src/commands/run.ts index 58e34be30..9f81cd271 100644 --- a/client/src/commands/run.ts +++ b/client/src/commands/run.ts @@ -24,10 +24,66 @@ import { RunResult, getSession, } from "../connection"; -import { getSelectedRegions } from "../utils/utils"; import { profileConfig, switchProfile } from "./profile"; let running = false; +interface FoldingBlock { + startLine: number; + startCol: number; + endLine: number; + endCol: number; +} + +export async function getSelectedRegions( + client: BaseLanguageClient, +): Promise { + const result: string[] = []; + + async function pushBlock(line: number, col: number) { + const block = await client.sendRequest( + "sas/getFoldingBlock", + { + textDocument: { uri: window.activeTextEditor.document.uri.toString() }, + line, + col, + }, + ); + if (block) { + const start = doc.offsetAt(new Position(block.startLine, block.startCol)); + const end = doc.offsetAt(new Position(block.endLine, block.endCol)); + const key = `${start}-${end}`; + if (result.indexOf(key) === -1) { + result.push(key); + } + return end; + } + } + + const editor = window.activeTextEditor; + const doc = editor.document; + for (const selection of editor.selections) { + const start = doc.offsetAt(selection.start); + let end = doc.offsetAt(selection.end); + const selectedText = doc.getText(selection); + if (selectedText.endsWith("\n")) { + --end; + } + for (let i = start; i <= end; i++) { + const pos = doc.positionAt(i); + const blockEnd = await pushBlock(pos.line, pos.character); + if (blockEnd && blockEnd > i) { + i = blockEnd; + } + } + } + return result.map((key) => { + const [start, end] = key.split("-"); + return new Selection( + doc.positionAt(parseInt(start)), + doc.positionAt(parseInt(end)), + ); + }); +} function getCode(selected = false, uri?: Uri): string { const editor = uri diff --git a/client/src/utils/utils.ts b/client/src/utils/utils.ts deleted file mode 100644 index 730f913de..000000000 --- a/client/src/utils/utils.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Position, Selection, window } from "vscode"; -import { BaseLanguageClient } from "vscode-languageclient"; - -interface FoldingBlock { - startLine: number; - startCol: number; - endLine: number; - endCol: number; -} - -export async function getSelectedRegions( - client: BaseLanguageClient, -): Promise { - const result: string[] = []; - - async function pushBlock(line: number, col: number) { - const block = await client.sendRequest( - "sas/getFoldingBlock", - { - textDocument: { uri: window.activeTextEditor.document.uri.toString() }, - line, - col, - }, - ); - if (block) { - const start = doc.offsetAt(new Position(block.startLine, block.startCol)); - const end = doc.offsetAt(new Position(block.endLine, block.endCol)); - const key = `${start}-${end}`; - if (result.indexOf(key) === -1) { - result.push(key); - } - return end; - } - } - - const editor = window.activeTextEditor; - const doc = editor.document; - for (const selection of editor.selections) { - const start = doc.offsetAt(selection.start); - let end = doc.offsetAt(selection.end); - const selectedText = doc.getText(selection); - if (selectedText.endsWith("\n")) { - --end; - } - for (let i = start; i <= end; i++) { - const pos = doc.positionAt(i); - const blockEnd = await pushBlock(pos.line, pos.character); - if (blockEnd && blockEnd > i) { - i = blockEnd; - } - } - } - return result.map((key) => { - const [start, end] = key.split("-"); - return new Selection( - doc.positionAt(parseInt(start)), - doc.positionAt(parseInt(end)), - ); - }); -} diff --git a/server/src/sas/LexerEx.ts b/server/src/sas/LexerEx.ts index 278f3eff6..1a64c5466 100644 --- a/server/src/sas/LexerEx.ts +++ b/server/src/sas/LexerEx.ts @@ -852,7 +852,14 @@ export class LexerEx { private resetFoldingBlockCache_() { this.sections = []; } - private tryEndFoldingBlock_(pos: TextPosition, untilType?: number) { + private tryEndFoldingBlock_( + pos: TextPosition, + untilType?: number, + lastPos?: TextPosition, + ) { + if (!lastPos) { + lastPos = pos; + } if (this.hasFoldingBlock_()) { // handle text end let secType = this.SEC_TYPE.PROC; @@ -868,7 +875,7 @@ export class LexerEx { if (untilType && this.currSection.type === untilType) { stop = true; } - this.endFoldingBlock_(secType, pos); + this.endFoldingBlock_(secType, stop ? lastPos : pos); } } } @@ -2929,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_, @@ -3083,10 +3089,11 @@ export class LexerEx { if ( this.searchBlockUpwardOfType_(this.currSection, this.SEC_TYPE.CUSTOM) ) { - if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { - this.endFoldingBlock_(this.SEC_TYPE.DATA, token.start); - } - this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); + this.tryEndFoldingBlock_( + token.start, + this.SEC_TYPE.CUSTOM, + token.end, + ); } } } @@ -3152,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_, @@ -3280,10 +3286,11 @@ export class LexerEx { if ( this.searchBlockUpwardOfType_(this.currSection, this.SEC_TYPE.CUSTOM) ) { - if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { - this.endFoldingBlock_(this.SEC_TYPE.DATA, token.start); - } - this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); + this.tryEndFoldingBlock_( + token.start, + this.SEC_TYPE.CUSTOM, + token.end, + ); } } } @@ -3457,10 +3464,11 @@ export class LexerEx { if ( this.searchBlockUpwardOfType_(this.currSection, this.SEC_TYPE.CUSTOM) ) { - if (this.currSection?.type !== this.SEC_TYPE.CUSTOM) { - this.endFoldingBlock_(this.SEC_TYPE.DATA, token.start); - } - this.tryEndFoldingBlock_(token.end, this.SEC_TYPE.CUSTOM); + this.tryEndFoldingBlock_( + token.start, + this.SEC_TYPE.CUSTOM, + token.end, + ); } } } @@ -3661,16 +3669,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, ); } } From 33cc5fd4c4bacc6c8c8b314745801ab30a968d32 Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Thu, 23 Nov 2023 17:07:56 +0800 Subject: [PATCH 13/17] fix: remove unused code --- client/src/commands/run.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/client/src/commands/run.ts b/client/src/commands/run.ts index 9f81cd271..9a56e50a6 100644 --- a/client/src/commands/run.ts +++ b/client/src/commands/run.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { EventEmitter, + Position, ProgressLocation, Selection, Uri, @@ -192,14 +193,7 @@ export async function runSelected(uri: Uri): Promise { export async function runRegion(client: BaseLanguageClient): Promise { const selections = await getSelectedRegions(client); const editor = window.activeTextEditor; - const doc = editor.document; - if (selections.length === 0) { - editor.selections = [ - new Selection(doc.positionAt(0), doc.positionAt(doc.getText().length)), - ]; - } else { - editor.selections = selections; - } + editor.selections = selections; await _run(true); } From 8226f74b3f18a32f572768667dfc8fdc9d7425b9 Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Tue, 28 Nov 2023 16:01:50 +0800 Subject: [PATCH 14/17] fix: revert client/src/run.ts --- client/src/commands/run.ts | 88 +++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/client/src/commands/run.ts b/client/src/commands/run.ts index 9a56e50a6..8c8dd8cf0 100644 --- a/client/src/commands/run.ts +++ b/client/src/commands/run.ts @@ -27,7 +27,6 @@ import { } from "../connection"; import { profileConfig, switchProfile } from "./profile"; -let running = false; interface FoldingBlock { startLine: number; startCol: number; @@ -35,7 +34,49 @@ interface FoldingBlock { endCol: number; } -export async function getSelectedRegions( +let running = false; + +function getCode(selected = false, uri?: Uri): string { + const editor = uri + ? window.visibleTextEditors.find( + (editor) => editor.document.uri.toString() === uri.toString(), + ) + : window.activeTextEditor; + const doc = editor?.document; + let codeFile = ""; + if (uri && uri.fsPath) { + codeFile = uri.fsPath; + } else if (doc) { + if (doc.fileName) { + codeFile = doc.fileName; + } else if (doc.uri && doc.uri.fsPath) { + codeFile = doc.uri.fsPath; + } + } + let code = ""; + if (selected) { + // run selected code if there is one or more non-empty selections, otherwise run all code + + // since you can have multiple selections, append the text for each selection in order of selection + // note: selection ranges can be empty (ex. just a carat) + for (const selection of editor.selections) { + const selectedText: string = doc.getText(selection); + code += selectedText; + } + // if no non-whitespace characters are selected, treat as no selection and run all code + if (code.trim().length === 0) { + code = doc?.getText(); + } + } else { + code = doc?.getText(); + } + if (codeFile) { + code = assign_SASProgramFile(code, codeFile); + } + return wrapCodeWithOutputHtml(code); +} + +async function getSelectedRegions( client: BaseLanguageClient, ): Promise { const result: string[] = []; @@ -86,46 +127,6 @@ export async function getSelectedRegions( }); } -function getCode(selected = false, uri?: Uri): string { - const editor = uri - ? window.visibleTextEditors.find( - (editor) => editor.document.uri.toString() === uri.toString(), - ) - : window.activeTextEditor; - const doc = editor?.document; - let codeFile = ""; - if (uri && uri.fsPath) { - codeFile = uri.fsPath; - } else if (doc) { - if (doc.fileName) { - codeFile = doc.fileName; - } else if (doc.uri && doc.uri.fsPath) { - codeFile = doc.uri.fsPath; - } - } - let code = ""; - if (selected) { - // run selected code if there is one or more non-empty selections, otherwise run all code - - // since you can have multiple selections, append the text for each selection in order of selection - // note: selection ranges can be empty (ex. just a carat) - for (const selection of editor.selections) { - const selectedText: string = doc.getText(selection); - code += selectedText; - } - // if no non-whitespace characters are selected, treat as no selection and run all code - if (code.trim().length === 0) { - code = doc?.getText(); - } - } else { - code = doc?.getText(); - } - if (codeFile) { - code = assign_SASProgramFile(code, codeFile); - } - return wrapCodeWithOutputHtml(code); -} - async function runCode(selected?: boolean, uri?: Uri) { if (profileConfig.getActiveProfile() === "") { switchProfile(); @@ -192,8 +193,7 @@ export async function runSelected(uri: Uri): Promise { export async function runRegion(client: BaseLanguageClient): Promise { const selections = await getSelectedRegions(client); - const editor = window.activeTextEditor; - editor.selections = selections; + window.activeTextEditor.selections = selections; await _run(true); } From 3fff6f1332d49fabc026d935b476c5bf64f458db Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Tue, 28 Nov 2023 16:30:23 +0800 Subject: [PATCH 15/17] fix: compile error --- client/src/node/extension.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/node/extension.ts b/client/src/node/extension.ts index 3d176ca3d..3d5bc63c4 100644 --- a/client/src/node/extension.ts +++ b/client/src/node/extension.ts @@ -48,7 +48,6 @@ import { NotebookSerializer } from "../components/notebook/Serializer"; import { ConnectionType } from "../components/profile"; import { SasTaskProvider } from "../components/tasks/SasTaskProvider"; import { SAS_TASK_TYPE } from "../components/tasks/SasTasks"; -import { getSelectedRegions } from "../utils/utils"; let client: LanguageClient; // Create Profile status bar item From 3331ad8139347e0beff1b09af77249933d7415dd Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Tue, 28 Nov 2023 16:32:45 +0800 Subject: [PATCH 16/17] fix: remove spaces --- client/src/node/extension.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/node/extension.ts b/client/src/node/extension.ts index 3d5bc63c4..3783fd084 100644 --- a/client/src/node/extension.ts +++ b/client/src/node/extension.ts @@ -131,7 +131,6 @@ export function activate(context: ExtensionContext): void { "SAS", new SASAuthProvider(context.secrets), ), - languages.registerDocumentSemanticTokensProvider( { language: "sas-log" }, LogTokensProvider, From 21cf2c5c5f0e3c98501b35b110c1f77f5a279a4f Mon Sep 17 00:00:00 2001 From: Jonas Jing Date: Mon, 4 Dec 2023 16:44:30 +0800 Subject: [PATCH 17/17] fix: start block when no cur block for gbl stmt --- server/src/sas/LexerEx.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/sas/LexerEx.ts b/server/src/sas/LexerEx.ts index 1a64c5466..93c5cf6ab 100644 --- a/server/src/sas/LexerEx.ts +++ b/server/src/sas/LexerEx.ts @@ -3546,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_,