diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 0d7f87a9be4..66a80b8867f 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -44,6 +44,7 @@ "terminalDataWriteEvent", "terminalDimensions", "testObserver", + "textDocumentEncoding", "textSearchProvider", "timeline", "tokenInformation", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 3fac3601f2b..b8d3ce3486a 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -619,7 +619,6 @@ suite('vscode API - workspace', () => { test('findFiles2, exclude', () => { return vscode.workspace.findFiles2(['**/image.png'], { exclude: ['**/sub/**'] }).then((res) => { - res.forEach(r => console.log(r.toString())); assert.strictEqual(res.length, 1); }); }); @@ -1305,4 +1304,26 @@ suite('vscode API - workspace', () => { disposeAll(disposables); return deleteFile(file); } + + test('text document encodings', async () => { + const uri1 = await createRandomFile(); + const uri2 = await createRandomFile(new Uint8Array([0xEF, 0xBB, 0xBF]) /* UTF-8 with BOM */); + const uri3 = await createRandomFile(new Uint8Array([0xFF, 0xFE]) /* UTF-16 LE BOM */); + const uri4 = await createRandomFile(new Uint8Array([0xFE, 0xFF]) /* UTF-16 BE BOM */); + + const doc1 = await vscode.workspace.openTextDocument(uri1); + assert.strictEqual(doc1.encoding, 'utf8'); + + const doc2 = await vscode.workspace.openTextDocument(uri2); + assert.strictEqual(doc2.encoding, 'utf8bom'); + + const doc3 = await vscode.workspace.openTextDocument(uri3); + assert.strictEqual(doc3.encoding, 'utf16le'); + + const doc4 = await vscode.workspace.openTextDocument(uri4); + assert.strictEqual(doc4.encoding, 'utf16be'); + + const doc5 = await vscode.workspace.openTextDocument({ content: 'Hello World' }); + assert.strictEqual(doc5.encoding, 'utf8'); + }); }); diff --git a/extensions/vscode-api-tests/src/utils.ts b/extensions/vscode-api-tests/src/utils.ts index 28cd422ce40..7223fa8a6a1 100644 --- a/extensions/vscode-api-tests/src/utils.ts +++ b/extensions/vscode-api-tests/src/utils.ts @@ -16,7 +16,7 @@ export function rndName() { export const testFs = new TestFS('fake-fs', true); vscode.workspace.registerFileSystemProvider(testFs.scheme, testFs, { isCaseSensitive: testFs.isCaseSensitive }); -export async function createRandomFile(contents = '', dir: vscode.Uri | undefined = undefined, ext = ''): Promise { +export async function createRandomFile(contents: string | Uint8Array = '', dir: vscode.Uri | undefined = undefined, ext = ''): Promise { let fakeFile: vscode.Uri; if (dir) { assert.strictEqual(dir.scheme, testFs.scheme); diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 5608a629539..f6c9174d732 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -361,6 +361,9 @@ const _allApiProposals = { testRelatedCode: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testRelatedCode.d.ts', }, + textDocumentEncoding: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts', + }, textEditorDiffInformation: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts', }, diff --git a/src/vs/workbench/api/browser/mainThreadDocuments.ts b/src/vs/workbench/api/browser/mainThreadDocuments.ts index 6ded63e34be..a882ce0beed 100644 --- a/src/vs/workbench/api/browser/mainThreadDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadDocuments.ts @@ -12,7 +12,8 @@ import { IModelService } from '../../../editor/common/services/model.js'; import { ITextModelService } from '../../../editor/common/services/resolverService.js'; import { IFileService, FileOperation } from '../../../platform/files/common/files.js'; import { ExtHostContext, ExtHostDocumentsShape, MainThreadDocumentsShape } from '../common/extHost.protocol.js'; -import { ITextFileService } from '../../services/textfile/common/textfiles.js'; +import { ITextFileEditorModel, ITextFileService } from '../../services/textfile/common/textfiles.js'; +import { IUntitledTextEditorModel } from '../../services/untitled/common/untitledTextEditorModel.js'; import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js'; import { toLocalResource, extUri, IExtUri } from '../../../base/common/resources.js'; import { IWorkingCopyFileService } from '../../services/workingCopy/common/workingCopyFileService.js'; @@ -145,6 +146,14 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen this._proxy.$acceptDirtyStateChanged(m.resource, m.isDirty()); } })); + this._store.add(Event.any(_textFileService.files.onDidChangeEncoding, _textFileService.untitled.onDidChangeEncoding)(m => { + if (this._shouldHandleFileEvent(m.resource)) { + const encoding = m.getEncoding(); + if (encoding) { + this._proxy.$acceptEncodingChanged(m.resource, encoding); + } + } + })); this._store.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => { const isMove = e.operation === FileOperation.MOVE; diff --git a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts index 66ae21fb2f8..69f487ee521 100644 --- a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts @@ -386,7 +386,8 @@ export class MainThreadDocumentsAndEditors { lines: model.getLinesContent(), EOL: model.getEOL(), languageId: model.getLanguageId(), - isDirty: this._textFileService.isDirty(model.uri) + isDirty: this._textFileService.isDirty(model.uri), + encoding: this._textFileService.getEncoding(model.uri) }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c9fab5d6adc..9b1f3d3b89c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1811,11 +1811,13 @@ export interface IModelAddedData { EOL: string; languageId: string; isDirty: boolean; + encoding: string; } export interface ExtHostDocumentsShape { $acceptModelLanguageChanged(strURL: UriComponents, newLanguageId: string): void; $acceptModelSaved(strURL: UriComponents): void; $acceptDirtyStateChanged(strURL: UriComponents, isDirty: boolean): void; + $acceptEncodingChanged(strURL: UriComponents, encoding: string): void; $acceptModelChanged(strURL: UriComponents, e: IModelChangedEvent, isDirty: boolean): void; } diff --git a/src/vs/workbench/api/common/extHostDocumentData.ts b/src/vs/workbench/api/common/extHostDocumentData.ts index c69c38d80ce..410c189527c 100644 --- a/src/vs/workbench/api/common/extHostDocumentData.ts +++ b/src/vs/workbench/api/common/extHostDocumentData.ts @@ -37,6 +37,7 @@ export class ExtHostDocumentData extends MirrorTextModel { uri: URI, lines: string[], eol: string, versionId: number, private _languageId: string, private _isDirty: boolean, + private _encoding: string ) { super(uri, lines, eol, versionId); } @@ -66,6 +67,7 @@ export class ExtHostDocumentData extends MirrorTextModel { get version() { return that._versionId; }, get isClosed() { return that._isDisposed; }, get isDirty() { return that._isDirty; }, + get encoding() { return that._encoding; }, save() { return that._save(); }, getText(range?) { return range ? that._getTextInRange(range) : that.getText(); }, get eol() { return that._eol === '\n' ? EndOfLine.LF : EndOfLine.CRLF; }, @@ -94,6 +96,11 @@ export class ExtHostDocumentData extends MirrorTextModel { this._isDirty = isDirty; } + _acceptEncoding(encoding: string): void { + ok(!this._isDisposed); + this._encoding = encoding; + } + private _save(): Promise { if (this._isDisposed) { return Promise.reject(new Error('Document has been closed')); diff --git a/src/vs/workbench/api/common/extHostDocuments.ts b/src/vs/workbench/api/common/extHostDocuments.ts index c26baab4a67..c0d0ca5ad9c 100644 --- a/src/vs/workbench/api/common/extHostDocuments.ts +++ b/src/vs/workbench/api/common/extHostDocuments.ts @@ -140,6 +140,20 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { }); } + public $acceptEncodingChanged(uriComponents: UriComponents, encoding: string): void { + const uri = URI.revive(uriComponents); + const data = this._documentsAndEditors.getDocument(uri); + if (!data) { + throw new Error('unknown document'); + } + data._acceptEncoding(encoding); + this._onDidChangeDocument.fire({ + document: data.document, + contentChanges: [], + reason: undefined + }); + } + public $acceptModelChanged(uriComponents: UriComponents, events: IModelChangedEvent, isDirty: boolean): void { const uri = URI.revive(uriComponents); const data = this._documentsAndEditors.getDocument(uri); diff --git a/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts b/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts index 5fe4b192750..81dd46d341b 100644 --- a/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts +++ b/src/vs/workbench/api/common/extHostDocumentsAndEditors.ts @@ -97,6 +97,7 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha data.versionId, data.languageId, data.isDirty, + data.encoding )); this._documents.set(resource, ref); addedDocuments.push(ref.value); diff --git a/src/vs/workbench/api/common/extHostInteractive.ts b/src/vs/workbench/api/common/extHostInteractive.ts index 0debcfa5b6c..9030149e350 100644 --- a/src/vs/workbench/api/common/extHostInteractive.ts +++ b/src/vs/workbench/api/common/extHostInteractive.ts @@ -52,6 +52,7 @@ export class ExtHostInteractive implements ExtHostInteractiveShape { uri: uri, isDirty: false, versionId: 1, + encoding: 'utf8' }] }); } diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index 205a899854c..333dad5de8d 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -40,7 +40,8 @@ export class ExtHostCell { languageId: cell.language, uri: cell.uri, isDirty: false, - versionId: 1 + versionId: 1, + encoding: 'utf8' }; } diff --git a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts index 2597ac759e5..2e10b368a64 100644 --- a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts +++ b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts @@ -167,6 +167,7 @@ suite('ExtHostLanguageFeatureCommands', function () { uri: model.uri, lines: model.getValue().split(model.getEOL()), EOL: model.getEOL(), + encoding: 'utf8' }] }); const extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); diff --git a/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts b/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts index 582a5e7c954..08fff8d0522 100644 --- a/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts +++ b/src/vs/workbench/api/test/browser/extHostBulkEdits.test.ts @@ -40,6 +40,7 @@ suite('ExtHostBulkEdits.applyWorkspaceEdit', () => { versionId: 1337, lines: ['foo'], EOL: '\n', + encoding: 'utf8' }] }); bulkEdits = new ExtHostBulkEdits(rpcProtocol, documentsAndEditors); diff --git a/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts index 5a128d5f781..4f83fc7d999 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentContentProvider.test.ts @@ -54,6 +54,7 @@ suite('ExtHostDocumentContentProvider', () => { versionId: 1, lines: ['foo'], EOL: '\n', + encoding: 'utf8' }] }); documentContentProvider = new ExtHostDocumentContentProvider(ehContext, documentsAndEditors, new NullLogService()); diff --git a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts index 926b8fdc6d2..5755dc3e1e8 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts @@ -37,7 +37,7 @@ suite('ExtHostDocumentData', () => { 'and this is line number two', //27 'it is followed by #3', //20 'and finished with the fourth.', //29 - ], '\n', 1, 'text', false); + ], '\n', 1, 'text', false, 'utf8'); }); ensureNoDisposablesAreLeakedInTestSuite(); @@ -59,7 +59,7 @@ suite('ExtHostDocumentData', () => { saved = uri; return Promise.resolve(true); } - }, URI.parse('foo:bar'), [], '\n', 1, 'text', true); + }, URI.parse('foo:bar'), [], '\n', 1, 'text', true, 'utf8'); return data.document.save().then(() => { assert.strictEqual(saved.toString(), 'foo:bar'); @@ -256,7 +256,7 @@ suite('ExtHostDocumentData', () => { test('getWordRangeAtPosition', () => { data = new ExtHostDocumentData(undefined!, URI.file(''), [ 'aaaa bbbb+cccc abc' - ], '\n', 1, 'text', false); + ], '\n', 1, 'text', false, 'utf8'); let range = data.document.getWordRangeAtPosition(new Position(0, 2))!; assert.strictEqual(range.start.line, 0); @@ -290,7 +290,7 @@ suite('ExtHostDocumentData', () => { 'function() {', ' "far boo"', '}' - ], '\n', 1, 'text', false); + ], '\n', 1, 'text', false, 'utf8'); let range = data.document.getWordRangeAtPosition(new Position(0, 0), /\/\*.+\*\//); assert.strictEqual(range, undefined); @@ -318,7 +318,7 @@ suite('ExtHostDocumentData', () => { data = new ExtHostDocumentData(undefined!, URI.file(''), [ perfData._$_$_expensive - ], '\n', 1, 'text', false); + ], '\n', 1, 'text', false, 'utf8'); // this test only ensures that we eventually give and timeout (when searching "funny" words and long lines) // for the sake of speedy tests we lower the timeBudget here @@ -345,7 +345,7 @@ suite('ExtHostDocumentData', () => { data = new ExtHostDocumentData(undefined!, URI.file(''), [ line - ], '\n', 1, 'text', false); + ], '\n', 1, 'text', false, 'utf8'); const range = data.document.getWordRangeAtPosition(new Position(0, 27), regex)!; assert.strictEqual(range.start.line, 0); @@ -358,7 +358,7 @@ suite('ExtHostDocumentData', () => { data = new ExtHostDocumentData(undefined!, URI.file(''), [ `

Sheldon, soprannominato "Shelly dalla madre e dalla sorella, è nato a Galveston, in Texas, il 26 febbraio 1980 in un supermercato. È stato un bambino prodigio, come testimoniato dal suo quoziente d'intelligenza (187, di molto superiore alla norma) e dalla sua rapida carriera scolastica: si è diplomato all'eta di 11 anni approdando alla stessa età alla formazione universitaria e all'età di 16 anni ha ottenuto il suo primo dottorato di ricerca. All'inizio della serie e per gran parte di essa vive con il coinquilino Leonard nell'appartamento 4A al 2311 North Los Robles Avenue di Pasadena, per poi trasferirsi nell'appartamento di Penny con Amy nella decima stagione. Come più volte afferma lui stesso possiede una memoria eidetica e un orecchio assoluto. È stato educato da una madre estremamente religiosa e, in più occasioni, questo aspetto contrasta con il rigore scientifico di Sheldon; tuttavia la donna sembra essere l'unica persona in grado di comandarlo a bacchetta.

` - ], '\n', 1, 'text', false); + ], '\n', 1, 'text', false, 'utf8'); const pos = new Position(0, 55); const range = data.document.getWordRangeAtPosition(pos)!; @@ -426,7 +426,7 @@ suite('ExtHostDocumentData updates line mapping', () => { } function testLineMappingDirectionAfterEvents(lines: string[], eol: string, direction: AssertDocumentLineMappingDirection, e: IModelChangedEvent): void { - const myDocument = new ExtHostDocumentData(undefined!, URI.file(''), lines.slice(0), eol, 1, 'text', false); + const myDocument = new ExtHostDocumentData(undefined!, URI.file(''), lines.slice(0), eol, 1, 'text', false, 'utf8'); assertDocumentLineMapping(myDocument, direction); myDocument.onEvents(e); diff --git a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts index 965bb16223e..f2ccb19be70 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts @@ -39,6 +39,7 @@ suite('ExtHostDocumentSaveParticipant', () => { versionId: 1, lines: ['foo'], EOL: '\n', + encoding: 'utf8' }] }); documents = new ExtHostDocuments(SingleProxyRPCProtocol(null), documentsAndEditors); diff --git a/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts index a0b9f1f7bef..ca8101f2076 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentsAndEditors.test.ts @@ -32,7 +32,8 @@ suite('ExtHostDocumentsAndEditors', () => { lines: [ 'first', 'second' - ] + ], + encoding: 'utf8' }] }); diff --git a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts index e26fa6617e0..7aa83e5df45 100644 --- a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts @@ -111,6 +111,7 @@ suite('ExtHostLanguageFeatures', function () { uri: model.uri, lines: model.getValue().split(model.getEOL()), EOL: model.getEOL(), + encoding: 'utf8' }] }); const extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); diff --git a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts index 54b14a349b4..0d7138473e6 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts @@ -220,7 +220,8 @@ suite('NotebookCell#Document', function () { lines: doc.getText().split('\n'), languageId: doc.languageId, uri: doc.uri, - versionId: doc.version + versionId: doc.version, + encoding: 'utf8' }); } diff --git a/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts b/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts index b76b9048aaa..58148a5164e 100644 --- a/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts @@ -19,7 +19,7 @@ suite('ExtHostTextEditor', () => { let editor: ExtHostTextEditor; const doc = new ExtHostDocumentData(undefined!, URI.file(''), [ 'aaaa bbbb+cccc abc' - ], '\n', 1, 'text', false); + ], '\n', 1, 'text', false, 'utf8'); setup(() => { editor = new ExtHostTextEditor('fake', null!, new NullLogService(), new Lazy(() => doc.document), [], { cursorStyle: TextEditorCursorStyle.Line, insertSpaces: true, lineNumbers: 1, tabSize: 4, indentSize: 4, originalIndentSize: 'tabSize' }, [], 1); diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts index e077dfc7ac7..26dd5eaf8f2 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts @@ -83,8 +83,13 @@ suite('MainThreadDocumentsAndEditors', () => { override files = { onDidSave: Event.None, onDidRevert: Event.None, - onDidChangeDirty: Event.None + onDidChangeDirty: Event.None, + onDidChangeEncoding: Event.None }; + override untitled = { + onDidChangeEncoding: Event.None + }; + override getEncoding() { return 'utf8'; } }; const workbenchEditorService = disposables.add(new TestEditorService()); const editorGroupService = new TestEditorGroupsService(); diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 9ba24786bc5..9b3b2aa04ec 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -270,6 +270,11 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return this.fileService.writeFile(resource, readable, options); } + getEncoding(resource: URI): string { + const model = resource.scheme === Schemas.untitled ? this.untitled.get(resource) : this.files.get(resource); + return model?.getEncoding() ?? this.encoding.getUnvalidatedEncodingForResource(resource); + } + async getEncodedReadable(resource: URI, value: ITextSnapshot): Promise; async getEncodedReadable(resource: URI, value: string): Promise; async getEncodedReadable(resource: URI, value?: ITextSnapshot): Promise; @@ -773,7 +778,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings { } async getPreferredWriteEncoding(resource: URI, preferredEncoding?: string): Promise { - const resourceEncoding = await this.getEncodingForResource(resource, preferredEncoding); + const resourceEncoding = await this.getValidatedEncodingForResource(resource, preferredEncoding); return { encoding: resourceEncoding, @@ -803,7 +808,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings { preferredEncoding = UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then } - const encoding = await this.getEncodingForResource(resource, preferredEncoding); + const encoding = await this.getValidatedEncodingForResource(resource, preferredEncoding); return { encoding, @@ -811,7 +816,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings { }; } - private async getEncodingForResource(resource: URI, preferredEncoding?: string): Promise { + getUnvalidatedEncodingForResource(resource: URI, preferredEncoding?: string): string { let fileEncoding: string; const override = this.getEncodingOverride(resource); @@ -823,10 +828,13 @@ export class EncodingOracle extends Disposable implements IResourceEncodings { fileEncoding = this.textResourceConfigurationService.getValue(resource, 'files.encoding'); // and last we check for settings } - if (fileEncoding !== UTF8) { - if (!fileEncoding || !(await encodingExists(fileEncoding))) { - fileEncoding = UTF8; // the default is UTF-8 - } + return fileEncoding || UTF8; + } + + private async getValidatedEncodingForResource(resource: URI, preferredEncoding?: string): Promise { + let fileEncoding = this.getUnvalidatedEncodingForResource(resource, preferredEncoding); + if (fileEncoding !== UTF8 && !(await encodingExists(fileEncoding))) { + fileEncoding = UTF8; } return fileEncoding; diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index d51b932693c..92e88eec942 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -98,6 +98,12 @@ export interface ITextFileService extends IDisposable { */ create(operations: { resource: URI; value?: string | ITextSnapshot; options?: { overwrite?: boolean } }[], undoInfo?: IFileOperationUndoRedoInfo): Promise; + /** + * Get the encoding for the provided `resource`. Will try to determine the encoding + * from any existing model for that `resource` and fallback to the configured defaults. + */ + getEncoding(resource: URI): string; + /** * Returns the readable that uses the appropriate encoding. This method should * be used whenever a `string` or `ITextSnapshot` is being persisted to the diff --git a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts index 18290d14a85..b27c93663a7 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts @@ -10,6 +10,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { TextFileEditorModel } from '../../common/textFileEditorModel.js'; import { FileOperation } from '../../../../../platform/files/common/files.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { EncodingMode } from '../../common/textfiles.js'; suite('Files - TextFileService', () => { @@ -199,5 +200,22 @@ suite('Files - TextFileService', () => { assert.strictEqual(suggested, 'plumbus'); }); + test('getEncoding() - files and untitled', async function () { + const model: TextFileEditorModel = disposables.add(instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined)); + (accessor.textFileService.files).add(model.resource, model); + + await model.resolve(); + + assert.strictEqual(accessor.textFileService.getEncoding(model.resource), 'utf8'); + await model.setEncoding('utf16', EncodingMode.Encode); + assert.strictEqual(accessor.textFileService.getEncoding(model.resource), 'utf16'); + + const untitled = disposables.add(await accessor.textFileService.untitled.resolve()); + + assert.strictEqual(accessor.textFileService.getEncoding(untitled.resource), 'utf8'); + await untitled.setEncoding('utf16'); + assert.strictEqual(accessor.textFileService.getEncoding(untitled.resource), 'utf16'); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts b/src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts new file mode 100644 index 00000000000..ebb5d2a5e0d --- /dev/null +++ b/src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/824 + + export interface TextDocument { + + /** + * The file encoding of this document that will be used when the document is saved. + * + * Use the {@link workspace.onDidChangeTextDocument onDidChangeTextDocument}-event to + * get notified when the document encoding changes. + * + * Note that the possible encoding values are currently defined as any of the following: + * 'utf8', 'utf8bom', 'utf16le', 'utf16be', 'windows1252', 'iso88591', 'iso88593', + * 'iso885915', 'macroman', 'cp437', 'windows1256', 'iso88596', 'windows1257', + * 'iso88594', 'iso885914', 'windows1250', 'iso88592', 'cp852', 'windows1251', + * 'cp866', 'cp1125', 'iso88595', 'koi8r', 'koi8u', 'iso885913', 'windows1253', + * 'iso88597', 'windows1255', 'iso88598', 'iso885910', 'iso885916', 'windows1254', + * 'iso88599', 'windows1258', 'gbk', 'gb18030', 'cp950', 'big5hkscs', 'shiftjis', + * 'eucjp', 'euckr', 'windows874', 'iso885911', 'koi8ru', 'koi8t', 'gb2312', + * 'cp865', 'cp850'. + */ + readonly encoding: string; + } +}