@@ -26,6 +26,7 @@ import { IQuartoKernelManager } from '../../browser/quartoKernelManager.js';
2626import { IQuartoDocumentModelService } from '../../browser/quartoDocumentModelService.js' ;
2727import { QuartoCodeCell } from '../../common/quartoTypes.js' ;
2828import { IEditorService } from '../../../../services/editor/common/editorService.js' ;
29+ import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js' ;
2930import { IEphemeralStateService } from '../../../../../platform/ephemeralState/common/ephemeralState.js' ;
3031import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js' ;
3132import { CancellationToken } from '../../../../../base/common/cancellation.js' ;
@@ -80,6 +81,7 @@ describe('QuartoExecutionManager', () => {
8081 let mockKernelManager : MockKernelManager ;
8182 let mockDocumentModelService : MockDocumentModelService ;
8283 let mockEditorService : MockEditorService ;
84+ let mockEditorGroupsService : MockEditorGroupsService ;
8385 let mockConsoleService : RecordingConsoleService ;
8486 let mockRuntimeSessionService : MockRuntimeSessionService ;
8587 let configurationService : TestConfigurationService ;
@@ -101,6 +103,7 @@ describe('QuartoExecutionManager', () => {
101103 mockKernelManager = ctx . disposables . add ( new MockKernelManager ( mockSession ) ) ;
102104 mockDocumentModelService = new MockDocumentModelService ( ) ;
103105 mockEditorService = new MockEditorService ( ) ;
106+ mockEditorGroupsService = new MockEditorGroupsService ( ) ;
104107 const mockEphemeralStateService = new MockEphemeralStateService ( ) ;
105108 const mockWorkspaceContextService = new MockWorkspaceContextService ( ) ;
106109 mockConsoleService = new RecordingConsoleService ( ) ;
@@ -120,6 +123,7 @@ describe('QuartoExecutionManager', () => {
120123 asKernelManager ( mockKernelManager ) ,
121124 asDocumentModelService ( mockDocumentModelService ) ,
122125 asEditorService ( mockEditorService ) ,
126+ asEditorGroupsService ( mockEditorGroupsService ) ,
123127 asEphemeralStateService ( mockEphemeralStateService ) ,
124128 asWorkspaceContextService ( mockWorkspaceContextService ) ,
125129 configurationService ,
@@ -1234,6 +1238,7 @@ describe('QuartoExecutionManager', () => {
12341238 asKernelManager ( mockKernelManager ) ,
12351239 asDocumentModelService ( mockDocumentModelService ) ,
12361240 asEditorService ( trackingEditorService ) ,
1241+ asEditorGroupsService ( new MockEditorGroupsService ( ) ) ,
12371242 asEphemeralStateService ( new MockEphemeralStateService ( ) ) ,
12381243 asWorkspaceContextService ( new MockWorkspaceContextService ( ) ) ,
12391244 configurationService ,
@@ -1333,6 +1338,7 @@ describe('QuartoExecutionManager', () => {
13331338 asKernelManager ( mockKernelManager ) ,
13341339 asDocumentModelService ( localMockDocumentModelService ) ,
13351340 asEditorService ( localMockEditorService ) ,
1341+ asEditorGroupsService ( new MockEditorGroupsService ( ) ) ,
13361342 asEphemeralStateService ( new MockEphemeralStateService ( ) ) ,
13371343 asWorkspaceContextService ( new MockWorkspaceContextService ( ) ) ,
13381344 configurationService ,
@@ -1506,6 +1512,67 @@ describe('QuartoExecutionManager', () => {
15061512 expect ( metadata ! [ 'output_pixel_ratio' ] , 'external-only key should pass through' ) . toBe ( 2 ) ;
15071513 } ) ;
15081514 } ) ;
1515+
1516+ describe ( 'Editor Pinning' , ( ) => {
1517+ it ( 'pins the document tab when a cell is executed (#14736)' , async ( ) => {
1518+ const documentUri = URI . file ( '/test-pin-cell.qmd' ) ;
1519+ const cell : QuartoCodeCell = {
1520+ id : 'test-pin-cell' ,
1521+ index : 0 ,
1522+ language : 'python' ,
1523+ startLine : 1 ,
1524+ endLine : 4 ,
1525+ codeStartLine : 2 ,
1526+ codeEndLine : 3 ,
1527+ label : undefined ,
1528+ options : '' ,
1529+ contentHash : 'pin-cell' ,
1530+ } ;
1531+
1532+ const executionPromise = executionManager . executeCell ( documentUri , cell ) ;
1533+ const executionId = await mockKernelManager . waitForExecution ( ) ;
1534+ mockSession . receiveStateMessage ( {
1535+ parent_id : executionId ,
1536+ state : RuntimeOnlineState . Idle ,
1537+ } ) ;
1538+ await executionPromise ;
1539+
1540+ expect ( mockEditorGroupsService . pinnedEditors . length ) . toBe ( 1 ) ;
1541+ } ) ;
1542+
1543+ it ( 'pins the document tab when inline cells are executed (#14736)' , async ( ) => {
1544+ const documentUri = URI . file ( '/test-pin-inline.qmd' ) ;
1545+ const cell : QuartoCodeCell = {
1546+ id : 'test-pin-inline' ,
1547+ index : 0 ,
1548+ language : 'python' ,
1549+ startLine : 1 ,
1550+ endLine : 3 ,
1551+ codeStartLine : 2 ,
1552+ codeEndLine : 2 ,
1553+ label : undefined ,
1554+ options : '' ,
1555+ contentHash : 'pin-inline' ,
1556+ } ;
1557+ const documentLines = [ '```{python}' , 'x = 1' , '```' ] ;
1558+ const mockModel = new MockQuartoDocumentModel ( [ cell ] , documentLines ) ;
1559+ mockDocumentModelService . setMockModel ( mockModel ) ;
1560+ mockEditorService . getValueInRangeCallback = ( range : unknown ) => {
1561+ const r = range as { startLineNumber : number ; endLineNumber : number } ;
1562+ return documentLines . slice ( r . startLineNumber - 1 , r . endLineNumber ) . join ( '\n' ) ;
1563+ } ;
1564+
1565+ const executionPromise = executionManager . executeInlineCells ( documentUri , [ new Range ( 2 , 1 , 2 , 100 ) ] ) ;
1566+ const executionId = await mockKernelManager . waitForExecution ( ) ;
1567+ mockSession . receiveStateMessage ( {
1568+ parent_id : executionId ,
1569+ state : RuntimeOnlineState . Idle ,
1570+ } ) ;
1571+ await executionPromise ;
1572+
1573+ expect ( mockEditorGroupsService . pinnedEditors . length ) . toBe ( 1 ) ;
1574+ } ) ;
1575+ } ) ;
15091576} ) ;
15101577
15111578// Mock implementations
@@ -1696,6 +1763,7 @@ class MockEditorService {
16961763 findEditors ( _resource : unknown ) : unknown [ ] {
16971764 const self = this ;
16981765 return [ {
1766+ groupId : MOCK_EDITOR_GROUP_ID ,
16991767 editor : {
17001768 resolve : async ( ) => ( {
17011769 textEditorModel : {
@@ -1727,6 +1795,35 @@ function asEditorService(mock: MockEditorService): IEditorService {
17271795 } ) ;
17281796}
17291797
1798+ /** Group id reported by MockEditorService.findEditors, resolved by MockEditorGroupsService.getGroup. */
1799+ const MOCK_EDITOR_GROUP_ID = 1 ;
1800+
1801+ /**
1802+ * Mock editor groups service that records every editor pinned via its group's
1803+ * pinEditor. Lets tests assert that running a cell pins the document's tab.
1804+ */
1805+ class MockEditorGroupsService {
1806+ readonly pinnedEditors : unknown [ ] = [ ] ;
1807+
1808+ private readonly _group = {
1809+ pinEditor : ( editor : unknown ) => {
1810+ this . pinnedEditors . push ( editor ) ;
1811+ } ,
1812+ } ;
1813+
1814+ getGroup ( groupId : number ) : unknown {
1815+ return groupId === MOCK_EDITOR_GROUP_ID ? this . _group : undefined ;
1816+ }
1817+ }
1818+
1819+ function asEditorGroupsService ( mock : MockEditorGroupsService ) : IEditorGroupsService {
1820+ return stubInterface < IEditorGroupsService > ( {
1821+ // Cast: the mock's group only implements pinEditor, which is all the
1822+ // execution manager calls; we narrow at the boundary.
1823+ getGroup : mock . getGroup . bind ( mock ) as IEditorGroupsService [ 'getGroup' ] ,
1824+ } ) ;
1825+ }
1826+
17301827class MockEphemeralStateService {
17311828 async setItem ( _key : string , _value : unknown ) : Promise < void > {
17321829 // No-op: production calls setItem for queue persistence; tests don't read it back.
0 commit comments