Code change:
return obj; } <del> export function getCellMetadata(cell: NotebookCell | NotebookCellData) { return { </del> <ins> export function getCellMetadata(cell: NotebookCell | NotebookCellData): CellMetadata { const metadata = { </ins> // it contains the cell id, and the cell metadata, along with other nb cell metadata <del> ...(cell.metadata?.custom ?? {}), // promote the cell attachments to the top level attachments: cell.metadata?.custom?.attachments ?? cell.metadata?.attachments </del> <ins> ...(cell.metadata?.custom ?? {}) </ins> }; <ins> // promote the cell attachments to the top level const attachments = cell.metadata?.custom?.attachments ?? cell.metadata?.attachments; if (attachments) { metadata.attachments = attachments; } return metadata; } export function getVSCodeCellLanguageId(metadata: CellMetadata): string | undefined { return metadata.metadata?.vscode?.languageId; } export function setVSCodeCellLanguageId(metadata: CellMetadata, languageId: string) { metadata.metadata = metadata.metadata || {}; metadata.metadata.vscode = { languageId }; } export function removeVSCodeCellLanguageId(metadata: CellMetadata) { if (metadata.metadata?.vscode) { delete metadata.metadata.vscode; } </ins> } function createCodeCellFromNotebookCell(cell: NotebookCellData, preferredLanguage: string | undefined): nbformat.ICodeCell { <del> const cellMetadata = getCellMetadata(cell); let metadata = cellMetadata?.metadata || {}; // This cannot be empty. </del> <ins> const cellMetadata: CellMetadata = JSON.parse(JSON.stringify(getCellMetadata(cell))); cellMetadata.metadata = cellMetadata.metadata || {}; // This cannot be empty. </ins> if (cell.languageId !== preferredLanguage) { <del> metadata = { ...metadata, vscode: { languageId: cell.languageId } }; } else if (metadata.vscode) { </del> <ins> setVSCodeCellLanguageId(cellMetadata, cell.languageId); } else { </ins> // cell current language is the same as the preferred cell language in the document, flush the vscode custom language id metadata <del> delete metadata.vscode; </del> <ins> removeVSCodeCellLanguageId(cellMetadata); </ins> } const codeCell: any = {

Related discussion:
text: Create an ipynb file (with one Python cell) Save this Verify the entry exists in time line Change the language of the cell to Save this Verify we have an entry Click that item Bug On the left we need On the right there's no such entry in metadata This is incorrect. The ipynb has the above metadata. What seems to be happening is When the language changes, We serialize the ipynb and JSon contains the metadata However the model is not updated with corresponding changes to the ipynb file I.e. model change triggered =ipynb serialize to JSON =Json contains new metadtaa not in model =Missing step is to update the model again If we implement the last step, we could always end up in an infinite loop, Perhaps we should first update the model with metadata when ever we detect chagnes in the model before the save event. E.g. as part of pre-Save (onDidChangeNotebookDocument &/or onWillSaveNotebookDocument) This way, model will always be accurate and upto date when ipynb is serialized.
Easier way to explain the bug Create an ipynb file (with one Python cell) Save this Change the language of the cell to Save this The cell metadata will not contain the new metadata that exists in the ipynb JSON file. (model is not in sync with ipynb json)
Suggested Solution Perform model updates when we're about to save (or when we detect changes to model) When saving, ensure we save the metadata as is, without generating any new content I.e. JSON.stringify the metadata.
/cc
Considering that a serialize operation should not have side effects (ideally), enforcing a "save participant" from ipynb extension makes very good sense to me. We can leverage which support additional async operations, like how we handle attachment clearnup
For such critical metadata change, we might want to handle both auto save and manual save as we want the json and model to be always in sync.
Another simple repro step Create a notebook, and save this Add a new cell The latest entry in timeline will be However there's a difference, one has and the other (current model state) does not have any Again, model does not contain metadta (as this is a new cell) and ipynb JSON contains metadata when serialized.