This commit is contained in:
Benjamin Pasero 2019-06-11 09:54:48 +02:00
parent 85d0ec7171
commit eabd2c51a4
24 changed files with 82 additions and 69 deletions

View file

@ -7,6 +7,9 @@ import { basename, posix, extname } from 'vs/base/common/path';
import { endsWith, startsWithUTF8BOM, startsWith } from 'vs/base/common/strings';
import { coalesce } from 'vs/base/common/arrays';
import { match } from 'vs/base/common/glob';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { DataUri } from 'vs/base/common/resources';
export const MIME_TEXT = 'text/plain';
export const MIME_BINARY = 'application/octet-stream';
@ -106,12 +109,28 @@ export function clearTextMimes(onlyUserConfigured?: boolean): void {
/**
* Given a file, return the best matching mime type for it
*/
export function guessMimeTypes(path: string | null, firstLine?: string): string[] {
export function guessMimeTypes(resource: URI | null, firstLine?: string): string[] {
let path: string | undefined;
if (resource) {
switch (resource.scheme) {
case Schemas.file:
path = resource.fsPath;
break;
case Schemas.data:
const metadata = DataUri.parseMetaData(resource);
path = metadata.get(DataUri.META_DATA_LABEL);
break;
default:
path = resource.path;
}
}
if (!path) {
return [MIME_UNKNOWN];
}
path = path.toLowerCase();
const filename = basename(path);
// 1.) User configured mappings have highest priority

View file

@ -4,42 +4,43 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { guessMimeTypes, registerTextMime, suggestFilename } from 'vs/base/common/mime';
import { URI } from 'vs/base/common/uri';
suite('Mime', () => {
test('Dynamically Register Text Mime', () => {
let guess = guessMimeTypes('foo.monaco');
let guess = guessMimeTypes(URI.file('foo.monaco'));
assert.deepEqual(guess, ['application/unknown']);
registerTextMime({ id: 'monaco', extension: '.monaco', mime: 'text/monaco' });
guess = guessMimeTypes('foo.monaco');
guess = guessMimeTypes(URI.file('foo.monaco'));
assert.deepEqual(guess, ['text/monaco', 'text/plain']);
guess = guessMimeTypes('.monaco');
guess = guessMimeTypes(URI.file('.monaco'));
assert.deepEqual(guess, ['text/monaco', 'text/plain']);
registerTextMime({ id: 'codefile', filename: 'Codefile', mime: 'text/code' });
guess = guessMimeTypes('Codefile');
guess = guessMimeTypes(URI.file('Codefile'));
assert.deepEqual(guess, ['text/code', 'text/plain']);
guess = guessMimeTypes('foo.Codefile');
guess = guessMimeTypes(URI.file('foo.Codefile'));
assert.deepEqual(guess, ['application/unknown']);
registerTextMime({ id: 'docker', filepattern: 'Docker*', mime: 'text/docker' });
guess = guessMimeTypes('Docker-debug');
guess = guessMimeTypes(URI.file('Docker-debug'));
assert.deepEqual(guess, ['text/docker', 'text/plain']);
guess = guessMimeTypes('docker-PROD');
guess = guessMimeTypes(URI.file('docker-PROD'));
assert.deepEqual(guess, ['text/docker', 'text/plain']);
registerTextMime({ id: 'niceregex', mime: 'text/nice-regex', firstline: /RegexesAreNice/ });
guess = guessMimeTypes('Randomfile.noregistration', 'RegexesAreNice');
guess = guessMimeTypes(URI.file('Randomfile.noregistration'), 'RegexesAreNice');
assert.deepEqual(guess, ['text/nice-regex', 'text/plain']);
guess = guessMimeTypes('Randomfile.noregistration', 'RegexesAreNotNice');
guess = guessMimeTypes(URI.file('Randomfile.noregistration'), 'RegexesAreNotNice');
assert.deepEqual(guess, ['application/unknown']);
guess = guessMimeTypes('Codefile', 'RegexesAreNice');
guess = guessMimeTypes(URI.file('Codefile'), 'RegexesAreNice');
assert.deepEqual(guess, ['text/code', 'text/plain']);
});
@ -47,20 +48,20 @@ suite('Mime', () => {
registerTextMime({ id: 'monaco', extension: '.monaco', mime: 'text/monaco' });
registerTextMime({ id: 'foobar', mime: 'text/foobar', firstline: /foobar/ });
let guess = guessMimeTypes('foo.monaco');
let guess = guessMimeTypes(URI.file('foo.monaco'));
assert.deepEqual(guess, ['text/monaco', 'text/plain']);
guess = guessMimeTypes('foo.monaco', 'foobar');
guess = guessMimeTypes(URI.file('foo.monaco'), 'foobar');
assert.deepEqual(guess, ['text/monaco', 'text/plain']);
registerTextMime({ id: 'docker', filename: 'dockerfile', mime: 'text/winner' });
registerTextMime({ id: 'docker', filepattern: 'dockerfile*', mime: 'text/looser' });
guess = guessMimeTypes('dockerfile');
guess = guessMimeTypes(URI.file('dockerfile'));
assert.deepEqual(guess, ['text/winner', 'text/plain']);
registerTextMime({ id: 'azure-looser', mime: 'text/azure-looser', firstline: /azure/ });
registerTextMime({ id: 'azure-winner', mime: 'text/azure-winner', firstline: /azure/ });
guess = guessMimeTypes('azure', 'azure');
guess = guessMimeTypes(URI.file('azure'), 'azure');
assert.deepEqual(guess, ['text/azure-winner', 'text/plain']);
});
@ -68,16 +69,16 @@ suite('Mime', () => {
registerTextMime({ id: 'monaco2', extension: '.monaco2', mime: 'text/monaco2' });
registerTextMime({ id: 'monaco2', filename: 'specific.monaco2', mime: 'text/specific-monaco2' });
assert.deepEqual(guessMimeTypes('specific.monaco2'), ['text/specific-monaco2', 'text/plain']);
assert.deepEqual(guessMimeTypes('foo.monaco2'), ['text/monaco2', 'text/plain']);
assert.deepEqual(guessMimeTypes(URI.file('specific.monaco2')), ['text/specific-monaco2', 'text/plain']);
assert.deepEqual(guessMimeTypes(URI.file('foo.monaco2')), ['text/monaco2', 'text/plain']);
});
test('Specificity priority 2', () => {
registerTextMime({ id: 'monaco3', filename: 'specific.monaco3', mime: 'text/specific-monaco3' });
registerTextMime({ id: 'monaco3', extension: '.monaco3', mime: 'text/monaco3' });
assert.deepEqual(guessMimeTypes('specific.monaco3'), ['text/specific-monaco3', 'text/plain']);
assert.deepEqual(guessMimeTypes('foo.monaco3'), ['text/monaco3', 'text/plain']);
assert.deepEqual(guessMimeTypes(URI.file('specific.monaco3')), ['text/specific-monaco3', 'text/plain']);
assert.deepEqual(guessMimeTypes(URI.file('foo.monaco3')), ['text/monaco3', 'text/plain']);
});
test('Mimes Priority - Longest Extension wins', () => {
@ -85,13 +86,13 @@ suite('Mime', () => {
registerTextMime({ id: 'monaco', extension: '.monaco.xml', mime: 'text/monaco-xml' });
registerTextMime({ id: 'monaco', extension: '.monaco.xml.build', mime: 'text/monaco-xml-build' });
let guess = guessMimeTypes('foo.monaco');
let guess = guessMimeTypes(URI.file('foo.monaco'));
assert.deepEqual(guess, ['text/monaco', 'text/plain']);
guess = guessMimeTypes('foo.monaco.xml');
guess = guessMimeTypes(URI.file('foo.monaco.xml'));
assert.deepEqual(guess, ['text/monaco-xml', 'text/plain']);
guess = guessMimeTypes('foo.monaco.xml.build');
guess = guessMimeTypes(URI.file('foo.monaco.xml.build'));
assert.deepEqual(guess, ['text/monaco-xml-build', 'text/plain']);
});
@ -99,7 +100,7 @@ suite('Mime', () => {
registerTextMime({ id: 'monaco', extension: '.monaco.xnl', mime: 'text/monaco', userConfigured: true });
registerTextMime({ id: 'monaco', extension: '.monaco.xml', mime: 'text/monaco-xml' });
let guess = guessMimeTypes('foo.monaco.xnl');
let guess = guessMimeTypes(URI.file('foo.monaco.xnl'));
assert.deepEqual(guess, ['text/monaco', 'text/plain']);
});
@ -107,7 +108,7 @@ suite('Mime', () => {
registerTextMime({ id: 'monaco', filepattern: '**/dot.monaco.xml', mime: 'text/monaco' });
registerTextMime({ id: 'other', filepattern: '*ot.other.xml', mime: 'text/other' });
let guess = guessMimeTypes('/some/path/dot.monaco.xml');
let guess = guessMimeTypes(URI.file('/some/path/dot.monaco.xml'));
assert.deepEqual(guess, ['text/monaco', 'text/plain']);
});
@ -115,10 +116,16 @@ suite('Mime', () => {
registerTextMime({ id: 'monaco', filepattern: '**/dot.monaco.xml', mime: 'text/monaco' });
registerTextMime({ id: 'other', filepattern: '**/dot.monaco.xml', mime: 'text/other' });
let guess = guessMimeTypes('/some/path/dot.monaco.xml');
let guess = guessMimeTypes(URI.file('/some/path/dot.monaco.xml'));
assert.deepEqual(guess, ['text/other', 'text/plain']);
});
test('Data URIs', () => {
registerTextMime({ id: 'data', extension: '.data', mime: 'text/data' });
assert.deepEqual(guessMimeTypes(URI.parse(`data:;label:something.data;description:data,`)), ['text/data', 'text/plain']);
});
test('Filename Suggestion - Suggest prefix only when there are no relevant extensions', () => {
const id = 'plumbus0';
const mime = `text/${id}`;

View file

@ -85,19 +85,7 @@ export function detectModeId(modelService: IModelService, modeService: IModeServ
}
// otherwise fallback to path based detection
let path: string | undefined;
if (resource.scheme === Schemas.data) {
const metadata = DataUri.parseMetaData(resource);
path = metadata.get(DataUri.META_DATA_LABEL);
} else {
path = resource.path.toLowerCase();
}
if (path) {
return modeService.getModeIdByFilepathOrFirstLine(path);
}
return null; // finally - we do not know the mode id
return modeService.getModeIdByFilepathOrFirstLine(resource);
}
export function cssEscape(val: string): string {

View file

@ -323,11 +323,11 @@ export class LanguagesRegistry extends Disposable {
return [];
}
public getModeIdsFromFilepathOrFirstLine(filepath: string | null, firstLine?: string): string[] {
if (!filepath && !firstLine) {
public getModeIdsFromFilepathOrFirstLine(resource: URI | null, firstLine?: string): string[] {
if (!resource && !firstLine) {
return [];
}
let mimeTypes = mime.guessMimeTypes(filepath, firstLine);
let mimeTypes = mime.guessMimeTypes(resource, firstLine);
return this.extractModeIds(mimeTypes.join(','));
}

View file

@ -41,7 +41,7 @@ export interface IModeService {
getMimeForMode(modeId: string): string | null;
getLanguageName(modeId: string): string | null;
getModeIdForLanguageName(alias: string): string | null;
getModeIdByFilepathOrFirstLine(filepath: string, firstLine?: string): string | null;
getModeIdByFilepathOrFirstLine(resource: URI, firstLine?: string): string | null;
getModeId(commaSeparatedMimetypesOrCommaSeparatedIds: string): string | null;
getLanguageIdentifier(modeId: string | LanguageId): LanguageIdentifier | null;
getConfigurationFiles(modeId: string): URI[];
@ -49,7 +49,7 @@ export interface IModeService {
// --- instantiation
create(commaSeparatedMimetypesOrCommaSeparatedIds: string | undefined): ILanguageSelection;
createByLanguageName(languageName: string): ILanguageSelection;
createByFilepathOrFirstLine(filepath: string | null, firstLine?: string): ILanguageSelection;
createByFilepathOrFirstLine(rsource: URI | null, firstLine?: string): ILanguageSelection;
triggerMode(commaSeparatedMimetypesOrCommaSeparatedIds: string): void;
}

View file

@ -94,8 +94,8 @@ export class ModeServiceImpl implements IModeService {
return this._registry.getModeIdForLanguageNameLowercase(alias);
}
public getModeIdByFilepathOrFirstLine(filepath: string | null, firstLine?: string): string | null {
const modeIds = this._registry.getModeIdsFromFilepathOrFirstLine(filepath, firstLine);
public getModeIdByFilepathOrFirstLine(resource: URI | null, firstLine?: string): string | null {
const modeIds = this._registry.getModeIdsFromFilepathOrFirstLine(resource, firstLine);
if (modeIds.length > 0) {
return modeIds[0];
@ -138,9 +138,9 @@ export class ModeServiceImpl implements IModeService {
});
}
public createByFilepathOrFirstLine(filepath: string | null, firstLine?: string): ILanguageSelection {
public createByFilepathOrFirstLine(resource: URI | null, firstLine?: string): ILanguageSelection {
return new LanguageSelection(this.onLanguagesMaybeChanged, () => {
const modeId = this.getModeIdByFilepathOrFirstLine(filepath, firstLine);
const modeId = this.getModeIdByFilepathOrFirstLine(resource, firstLine);
return this._createModeAndGetLanguageIdentifier(modeId);
});
}

View file

@ -50,7 +50,7 @@ export class TextResourceConfigurationService extends Disposable implements ITex
if (model) {
return position ? this.modeService.getLanguageIdentifier(model.getLanguageIdAtPosition(position.lineNumber, position.column))!.language : model.getLanguageIdentifier().language;
}
return this.modeService.getModeIdByFilepathOrFirstLine(resource.path);
return this.modeService.getModeIdByFilepathOrFirstLine(resource);
}
}

View file

@ -159,7 +159,7 @@ class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorC
wordRange = new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn);
}
const modeId = this.modeService.getModeIdByFilepathOrFirstLine(textEditorModel.uri.fsPath);
const modeId = this.modeService.getModeIdByFilepathOrFirstLine(textEditorModel.uri);
this.addDecoration(
wordRange,
new MarkdownString().appendCodeblock(modeId ? modeId : '', previewValue)

View file

@ -152,15 +152,13 @@ export function createModel(value: string, language?: string, uri?: URI): ITextM
value = value || '';
if (!language) {
let path = uri ? uri.path : null;
let firstLF = value.indexOf('\n');
let firstLine = value;
if (firstLF !== -1) {
firstLine = value.substring(0, firstLF);
}
return doCreateModel(value, StaticServices.modeService.get().createByFilepathOrFirstLine(path, firstLine), uri);
return doCreateModel(value, StaticServices.modeService.get().createByFilepathOrFirstLine(uri || null, firstLine), uri);
}
return doCreateModel(value, StaticServices.modeService.get().create(language), uri);
}

View file

@ -45,7 +45,7 @@ export class MainThreadDocumentContentProviders implements MainThreadDocumentCon
return this._proxy.$provideTextDocumentContent(handle, uri).then(value => {
if (typeof value === 'string') {
const firstLineText = value.substr(0, 1 + value.search(/\r?\n/));
const languageSelection = this._modeService.createByFilepathOrFirstLine(uri.fsPath, firstLineText);
const languageSelection = this._modeService.createByFilepathOrFirstLine(uri, firstLineText);
return this._modelService.createModel(value, languageSelection, uri);
}
return null;

View file

@ -538,7 +538,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
const resource = editor.getResource();
const path = resource ? resource.scheme === Schemas.file ? resource.fsPath : resource.path : undefined;
if (resource && path) {
descriptor['resource'] = { mimeType: guessMimeTypes(path).join(', '), scheme: resource.scheme, ext: extname(resource), path: hash(path) };
descriptor['resource'] = { mimeType: guessMimeTypes(resource).join(', '), scheme: resource.scheme, ext: extname(resource), path: hash(path) };
/* __GDPR__FRAGMENT__
"EditorTelemetryDescriptor" : {

View file

@ -1026,7 +1026,7 @@ export class ChangeModeAction extends Action {
if (textModel) {
const resource = toResource(activeEditor, { supportSideBySide: SideBySideEditor.MASTER });
if (resource) {
languageSelection = this.modeService.createByFilepathOrFirstLine(resource.fsPath, textModel.getLineContent(1));
languageSelection = this.modeService.createByFilepathOrFirstLine(resource, textModel.getLineContent(1));
}
}
} else {
@ -1044,7 +1044,7 @@ export class ChangeModeAction extends Action {
private configureFileAssociation(resource: URI): void {
const extension = extname(resource);
const base = basename(resource);
const currentAssociation = this.modeService.getModeIdByFilepathOrFirstLine(base);
const currentAssociation = this.modeService.getModeIdByFilepathOrFirstLine(resource.with({ path: base }));
const languages = this.modeService.getRegisteredLanguageNames();
const picks: IQuickPickItem[] = languages.sort().map((lang, index) => {

View file

@ -11,6 +11,7 @@ import { IModeService, ILanguageSelection } from 'vs/editor/common/services/mode
import { IModelService } from 'vs/editor/common/services/modelService';
import { IDisposable } from 'vs/base/common/lifecycle';
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
import { withUndefinedAsNull } from 'vs/base/common/types';
/**
* The base text editor model leverages the code editor model. This class is only intended to be subclassed and not instantiated.
@ -126,7 +127,7 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd
// lookup mode via resource path if the provided mode is unspecific
if (!preferredMode || preferredMode === PLAINTEXT_MODE_ID) {
return modeService.createByFilepathOrFirstLine(resource ? resource.path : null, firstLineText);
return modeService.createByFilepathOrFirstLine(withUndefinedAsNull(resource), firstLineText);
}
// otherwise take the preferred mode for granted

View file

@ -56,7 +56,7 @@ export class ResourceContextKey extends Disposable implements IContextKey<URI> {
this._register(_modeService.onDidCreateMode(() => {
const value = this._resourceKey.get();
this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value.fsPath) : null);
this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null);
}));
}
@ -65,7 +65,7 @@ export class ResourceContextKey extends Disposable implements IContextKey<URI> {
this._resourceKey.set(value);
this._schemeKey.set(value ? value.scheme : null);
this._filenameKey.set(value ? basename(value) : null);
this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value.fsPath) : null);
this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null);
this._extensionKey.set(value ? extname(value) : null);
this._hasResource.set(!!value);
this._isFileSystemResource.set(value ? this._fileService.canHandleResource(value) : false);

View file

@ -420,7 +420,7 @@ export class CommentNode extends Disposable {
const container = dom.append(this._commentEditContainer, dom.$('.edit-textarea'));
this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, container, SimpleCommentEditor.getEditorOptions(), this.parentEditor, this.parentThread);
const resource = URI.parse(`comment:commentinput-${this.comment.commentId}-${Date.now()}.md`);
this._commentEditorModel = this.modelService.createModel('', this.modeService.createByFilepathOrFirstLine(resource.path), resource, false);
this._commentEditorModel = this.modelService.createModel('', this.modeService.createByFilepathOrFirstLine(resource), resource, false);
this._commentEditor.setModel(this._commentEditorModel);
this._commentEditor.setValue(this.comment.body.value);

View file

@ -458,7 +458,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget
resource = resource.with({ authority: commentController.id });
}
const model = this.modelService.createModel(this._pendingComment || '', this.modeService.createByFilepathOrFirstLine(resource.path), resource, false);
const model = this.modelService.createModel(this._pendingComment || '', this.modeService.createByFilepathOrFirstLine(resource), resource, false);
this._disposables.add(model);
this._commentEditor.setModel(model);
this._disposables.add(this._commentEditor);

View file

@ -133,7 +133,7 @@ export class DebugContentProvider implements IWorkbenchContribution, ITextModelC
});
} else {
// create text model
const mime = response.body.mimeType || guessMimeTypes(resource.path)[0];
const mime = response.body.mimeType || guessMimeTypes(resource)[0];
const languageSelection = this.modeService.create(mime);
return this.modelService.createModel(response.body.content, languageSelection, resource);
}

View file

@ -609,7 +609,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
}
await this.extensionService.whenInstalledExtensionsRegistered();
const mimeTypes = await guessMimeTypes(uri.fsPath);
const mimeTypes = guessMimeTypes(uri);
if (mimeTypes.length !== 1 || mimeTypes[0] !== MIME_UNKNOWN) {
return;
}

View file

@ -610,7 +610,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
const keywords = lookup[ext] || [];
// Get mode name
const modeId = this.modeService.getModeIdByFilepathOrFirstLine(`.${ext}`);
const modeId = this.modeService.getModeIdByFilepathOrFirstLine(URI.file(`.${ext}`));
const languageName = modeId && this.modeService.getLanguageName(modeId);
const languageTag = languageName ? ` tag:"${languageName}"` : '';

View file

@ -837,7 +837,7 @@ class ClipboardContentProvider implements ITextModelContentProvider {
) { }
provideTextContent(resource: URI): Promise<ITextModel> {
const model = this.modelService.createModel(this.clipboardService.readText(), this.modeService.createByFilepathOrFirstLine(resource.path), resource);
const model = this.modelService.createModel(this.clipboardService.readText(), this.modeService.createByFilepathOrFirstLine(resource), resource);
return Promise.resolve(model);
}

View file

@ -202,7 +202,7 @@ export class TextFileContentProvider implements ITextModelContentProvider {
if (textFileModel) {
languageSelector = this.modeService.create(textFileModel.getModeId());
} else {
languageSelector = this.modeService.createByFilepathOrFirstLine(savedFileResource.path);
languageSelector = this.modeService.createByFilepathOrFirstLine(savedFileResource);
}
codeEditorModel = this.modelService.createModel(content.value, languageSelector, resource);

View file

@ -214,7 +214,7 @@ class Snapper {
}
public captureSyntaxTokens(fileName: string, content: string): Promise<IToken[]> {
const modeId = this.modeService.getModeIdByFilepathOrFirstLine(fileName);
const modeId = this.modeService.getModeIdByFilepathOrFirstLine(URI.file(fileName));
return this.textMateService.createGrammar(modeId!).then((grammar) => {
let lines = content.split(/\r\n|\r|\n/);

View file

@ -39,7 +39,7 @@ export class WalkThroughContentProvider implements ITextModelContentProvider, IW
return content.then(content => {
let codeEditorModel = this.modelService.getModel(resource);
if (!codeEditorModel) {
codeEditorModel = this.modelService.createModel(content, this.modeService.createByFilepathOrFirstLine(resource.fsPath), resource);
codeEditorModel = this.modelService.createModel(content, this.modeService.createByFilepathOrFirstLine(resource), resource);
} else {
this.modelService.updateModel(codeEditorModel, content);
}

View file

@ -821,7 +821,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
const fileName = basename(this.resource);
const path = this.resource.scheme === Schemas.file ? this.resource.fsPath : this.resource.path;
const telemetryData = {
mimeType: guessMimeTypes(path).join(', '),
mimeType: guessMimeTypes(this.resource).join(', '),
ext,
path: hash(path),
reason