From 7eb1f4d4850de59736240c7c1b08dd1f320af460 Mon Sep 17 00:00:00 2001 From: Lukas Streckeisen Date: Mon, 28 Apr 2025 11:43:44 +0200 Subject: [PATCH 1/3] override default behavior of langium folding provider, so the first line of a block remains visible --- src/language/ContextMapperDslModule.ts | 4 ++- .../ContextMapperDslFoldingRageProvider.ts | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/language/folding/ContextMapperDslFoldingRageProvider.ts diff --git a/src/language/ContextMapperDslModule.ts b/src/language/ContextMapperDslModule.ts index cf3853b..e1e6244 100644 --- a/src/language/ContextMapperDslModule.ts +++ b/src/language/ContextMapperDslModule.ts @@ -14,6 +14,7 @@ import { SemanticTokenProviderRegistry } from './semantictokens/SemanticTokenPro import { ContextMapperDslValidationRegistry } from './validation/ContextMapperDslValidationRegistry.js' import { ContextMapperValidationProviderRegistry } from './validation/ContextMapperValidationProviderRegistry.js' import { ContextMapperDslScopeProvider } from './references/ContextMapperDslScopeProvider.js' +import { ContextMapperDslFoldingRangeProvider } from './folding/ContextMapperDslFoldingRageProvider.js' /** * Declaration of custom services - add your own service classes here. @@ -47,7 +48,8 @@ export const ContextMapperDslModule: Module new ContextMapperDslValidationRegistry(services) }, lsp: { - SemanticTokenProvider: (services) => new ContextMapperDslSemanticTokenProvider(services, semanticTokenProviderRegistry) + SemanticTokenProvider: (services) => new ContextMapperDslSemanticTokenProvider(services, semanticTokenProviderRegistry), + FoldingRangeProvider: (services) => new ContextMapperDslFoldingRangeProvider(services) }, references: { ScopeProvider: (services) => new ContextMapperDslScopeProvider(services) diff --git a/src/language/folding/ContextMapperDslFoldingRageProvider.ts b/src/language/folding/ContextMapperDslFoldingRageProvider.ts new file mode 100644 index 0000000..a573630 --- /dev/null +++ b/src/language/folding/ContextMapperDslFoldingRageProvider.ts @@ -0,0 +1,25 @@ +import { DefaultFoldingRangeProvider } from 'langium/lsp' +import { CstNode, LangiumDocument } from 'langium' +import { FoldingRange } from 'vscode-languageserver-types' + +export class ContextMapperDslFoldingRangeProvider extends DefaultFoldingRangeProvider { + // modified version of DefaultFoldingRangeProvider#toFoldingRange + protected override toFoldingRange (document: LangiumDocument, node: CstNode, kind?: string): FoldingRange | undefined { + const range = node.range + let start = range.start + let end = range.end + + // Don't generate foldings for nodes that are less than 3 lines + if (end.line - start.line < 2) { + return undefined + } + + if (!this.includeLastFoldingLine(node, kind)) { // checks if block is parenthesis/bracket/brace block + // To prevent something like '...}' in the editor when a code block is collapsed, we want to still show the first line of a code block + start = document.textDocument.positionAt(document.textDocument.offsetAt({ line: start.line + 1, character: 0 }) - 1) + // As we don't want to hide the end token like 'if { ... --> } <--', we simply select the end of the previous line as the end position + end = document.textDocument.positionAt(document.textDocument.offsetAt({ line: end.line, character: 0 }) - 1) + } + return FoldingRange.create(start.line, end.line, start.character, end.character, kind) + } +} From ec5ffa66e603cca9e7c3f0b2a66fa40d66323fe8 Mon Sep 17 00:00:00 2001 From: Lukas Streckeisen Date: Mon, 28 Apr 2025 13:26:00 +0200 Subject: [PATCH 2/3] adjust end position for comment block folding range --- .../ContextMapperDslFoldingRageProvider.ts | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/language/folding/ContextMapperDslFoldingRageProvider.ts b/src/language/folding/ContextMapperDslFoldingRageProvider.ts index a573630..4485219 100644 --- a/src/language/folding/ContextMapperDslFoldingRageProvider.ts +++ b/src/language/folding/ContextMapperDslFoldingRageProvider.ts @@ -1,6 +1,6 @@ import { DefaultFoldingRangeProvider } from 'langium/lsp' import { CstNode, LangiumDocument } from 'langium' -import { FoldingRange } from 'vscode-languageserver-types' +import { FoldingRange, FoldingRangeKind } from 'vscode-languageserver-types' export class ContextMapperDslFoldingRangeProvider extends DefaultFoldingRangeProvider { // modified version of DefaultFoldingRangeProvider#toFoldingRange @@ -14,11 +14,25 @@ export class ContextMapperDslFoldingRangeProvider extends DefaultFoldingRangePro return undefined } - if (!this.includeLastFoldingLine(node, kind)) { // checks if block is parenthesis/bracket/brace block + if (!this.includeLastFoldingLine(node, kind)) { // checks if block is parenthesis/bracket/brace block or comment // To prevent something like '...}' in the editor when a code block is collapsed, we want to still show the first line of a code block - start = document.textDocument.positionAt(document.textDocument.offsetAt({ line: start.line + 1, character: 0 }) - 1) - // As we don't want to hide the end token like 'if { ... --> } <--', we simply select the end of the previous line as the end position - end = document.textDocument.positionAt(document.textDocument.offsetAt({ line: end.line, character: 0 }) - 1) + start = document.textDocument.positionAt(document.textDocument.offsetAt({ + line: start.line + 1, + character: 0 + }) - 1) + + let offsetFromEnd + if (kind === FoldingRangeKind.Comment) { + // So that comments are collapsed like '/*...*/' the end character needs to be at the start of the last line + offsetFromEnd = 0 + } else { + // As we don't want to hide the end token like 'if { ... --> } <--', we simply select the end of the previous line as the end position + offsetFromEnd = 1 + } + end = document.textDocument.positionAt(document.textDocument.offsetAt({ + line: end.line, + character: 0 + }) - offsetFromEnd) } return FoldingRange.create(start.line, end.line, start.character, end.character, kind) } From 624d30840a8bb36f003814d20252802d100f978c Mon Sep 17 00:00:00 2001 From: Lukas Streckeisen Date: Mon, 28 Apr 2025 13:26:07 +0200 Subject: [PATCH 3/3] add folding tests --- test/folding/CommentBlockFolding.test.ts | 92 +++++++++++++++++++++ test/folding/DefinitionBlockFolding.test.ts | 70 ++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 test/folding/CommentBlockFolding.test.ts create mode 100644 test/folding/DefinitionBlockFolding.test.ts diff --git a/test/folding/CommentBlockFolding.test.ts b/test/folding/CommentBlockFolding.test.ts new file mode 100644 index 0000000..18164d4 --- /dev/null +++ b/test/folding/CommentBlockFolding.test.ts @@ -0,0 +1,92 @@ +import { createContextMapperDslServices } from '../../src/language/ContextMapperDslModule.js' +import { FoldingRangeProvider } from 'langium/lsp' +import { clearDocuments, parseHelper } from 'langium/test' +import { ContextMappingModel } from '../../src/language/generated/ast.js' +import { EmptyFileSystem, LangiumDocument } from 'langium' +import { afterEach, beforeAll, describe, expect, test } from 'vitest' +import { parseValidInput } from '../ParsingTestHelper.js' +import { FoldingRangeParams } from 'vscode-languageserver' + +let services: ReturnType +let foldingRangeProvider: FoldingRangeProvider +let parse: ReturnType> +let document: LangiumDocument | undefined + +beforeAll(async () => { + services = createContextMapperDslServices(EmptyFileSystem) + foldingRangeProvider = services.ContextMapperDsl.lsp.FoldingRangeProvider! + parse = parseHelper(services.ContextMapperDsl) +}) + +afterEach(async () => { + document && await clearDocuments(services.shared, [document]) +}) + +describe('Comment block folding tests', () => { + test('check folding range of comment block', async () => { + document = await parseValidInput(parse, ` + /* + This is a comment block + */ + BoundedContext TestContext + `) + + const params = createFoldingRangeParams(document) + const foldingRanges = await foldingRangeProvider.getFoldingRanges(document, params) + expect(foldingRanges).toHaveLength(1) + expect(foldingRanges[0].startLine).toEqual(1) + expect(foldingRanges[0].startCharacter).toEqual(8) + expect(foldingRanges[0].endLine).toEqual(3) + expect(foldingRanges[0].endCharacter).toEqual(0) + }) + + test('check folding range of multiline comment', async () => { + document = await parseValidInput(parse, ` + BoundedContext TestContext { + /* This is a + looooonger + multiline comment + */ + Aggregate TestAggregate + } + `) + + const params = createFoldingRangeParams(document) + const foldingRanges = await foldingRangeProvider.getFoldingRanges(document, params) + expect(foldingRanges).toHaveLength(2) + expect(foldingRanges[1].startLine).toEqual(2) + expect(foldingRanges[1].startCharacter).toEqual(20) + expect(foldingRanges[1].endLine).toEqual(5) + expect(foldingRanges[1].endCharacter).toEqual(0) + }) + + test('check folding range of single-line comment', async () => { + document = await parseValidInput(parse, ` + // This is a single-line comment + BoundedContext TestContext + `) + + const params = createFoldingRangeParams(document) + const foldingRanges = await foldingRangeProvider.getFoldingRanges(document, params) + expect(foldingRanges).toHaveLength(0) + }) + + test('check folding range of single line comment block', async () => { + document = await parseValidInput(parse, ` + /* This is a single-line comment */ + BoundedContext TestContext + `) + + const params = createFoldingRangeParams(document) + const foldingRanges = await foldingRangeProvider.getFoldingRanges(document, params) + expect(foldingRanges).toHaveLength(0) + }) +}) + +function createFoldingRangeParams (document: LangiumDocument): FoldingRangeParams { + return { + textDocument: { + uri: document.uri.path + } + } +} diff --git a/test/folding/DefinitionBlockFolding.test.ts b/test/folding/DefinitionBlockFolding.test.ts new file mode 100644 index 0000000..5310e8f --- /dev/null +++ b/test/folding/DefinitionBlockFolding.test.ts @@ -0,0 +1,70 @@ +import { afterEach, beforeAll, describe, expect, test } from 'vitest' +import { createContextMapperDslServices } from '../../src/language/ContextMapperDslModule.js' +import { EmptyFileSystem, LangiumDocument } from 'langium' +import { FoldingRangeProvider } from 'langium/lsp' +import { clearDocuments, parseHelper } from 'langium/test' +import { ContextMappingModel } from '../../src/language/generated/ast.js' +import { parseValidInput } from '../ParsingTestHelper.js' +import { FoldingRangeParams } from 'vscode-languageserver' + +let services: ReturnType +let foldingRangeProvider: FoldingRangeProvider +let parse: ReturnType> +let document: LangiumDocument | undefined + +beforeAll(async () => { + services = createContextMapperDslServices(EmptyFileSystem) + foldingRangeProvider = services.ContextMapperDsl.lsp.FoldingRangeProvider! + parse = parseHelper(services.ContextMapperDsl) +}) + +afterEach(async () => { + document && await clearDocuments(services.shared, [document]) +}) + +describe('Definition block folding tests', () => { + test('check folding range of definition without body', async () => { + document = await parseValidInput(parse, ` + BoundedContext TestContext + `) + + const params = createFoldingRangeParams(document) + const foldingRanges = await foldingRangeProvider.getFoldingRanges(document, params) + expect(foldingRanges).toHaveLength(0) + }) + + test('check folding range of definition with one line body', async () => { + document = await parseValidInput(parse, ` + BoundedContext TestContext { + } + `) + + const params = createFoldingRangeParams(document) + const foldingRanges = await foldingRangeProvider.getFoldingRanges(document, params) + expect(foldingRanges).toHaveLength(0) + }) + + test('check folding range of definition with two line body', async () => { + document = await parseValidInput(parse, ` + BoundedContext TestContext { + type TEAM + } + `) + + const params = createFoldingRangeParams(document) + const foldingRanges = await foldingRangeProvider.getFoldingRanges(document, params) + expect(foldingRanges).toHaveLength(1) + expect(foldingRanges[0].startLine).toEqual(1) + expect(foldingRanges[0].startCharacter).toEqual(34) + expect(foldingRanges[0].endLine).toEqual(2) + expect(foldingRanges[0].endCharacter).toEqual(17) + }) +}) + +function createFoldingRangeParams (document: LangiumDocument): FoldingRangeParams { + return { + textDocument: { + uri: document.uri.path + } + } +}