From 27870487ce7967e2b6be85c555158d139c93cd5f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 4 Mar 2016 11:32:10 +0100 Subject: [PATCH] user configured mimes get highest priority --- src/vs/base/common/mime.ts | 195 ++++++++---------- src/vs/base/test/common/mime.test.ts | 44 ++-- src/vs/base/test/node/mime/mime.test.ts | 3 +- .../common/services/languagesRegistry.ts | 8 +- .../files/common/editors/fileAssociations.ts | 2 +- 5 files changed, 121 insertions(+), 131 deletions(-) diff --git a/src/vs/base/common/mime.ts b/src/vs/base/common/mime.ts index 95555ff50e1..8351529ad97 100644 --- a/src/vs/base/common/mime.ts +++ b/src/vs/base/common/mime.ts @@ -13,140 +13,83 @@ export let MIME_TEXT = 'text/plain'; export let MIME_BINARY = 'application/octet-stream'; export let MIME_UNKNOWN = 'application/unknown'; +export interface ITextMimeAssociation { + pattern?: string; + firstLineRegExp?: RegExp; + userConfigured?: boolean; +} + const registeredTextMimesByFilename: { [str: string]: string; } = Object.create(null); +const userConfiguredTextMimesByFilename: { [str: string]: string; } = Object.create(null); const registeredTextMimesByFirstLine: { regexp: RegExp; mime: string; }[] = []; -// This is for automatic generation at native.guplfile.js#41 => darwinBundleDocumentTypes.extensions -export function generateKnownFilenames(onlyExtensions: boolean = true): any { - let filter = (ext: string) => { - if (onlyExtensions) { - return /^\./.test(ext); - } - return true; - }; - let removeLeadingDot = (ext: string) => { - return ext.replace(/^\./, ''); - }; - - let list: string[] = []; - list = list.concat(Object.keys(registeredTextMimesByFilename)); - - list = list.filter(filter).map(removeLeadingDot); - list.sort(); - - let result: string[] = []; - let currentLetter: string = null; - let previousItem: string = null; - let currentRow: string[] = []; - - let pushCurrentRow = () => { - if (currentRow.length > 0) { - result.push('\'' + currentRow.join('\', \'') + '\''); - } - }; +/** + * Associate a text mime to the registry + */ +export function registerTextMime(mime: string, association: ITextMimeAssociation): void { + if (mime && association) { - for (let i = 0, len = list.length; i < len; i++) { - let item = list[i]; - if (item.length === 0) { - continue; + // Firstline pattern + if (association.firstLineRegExp) { + registeredTextMimesByFirstLine.push({ regexp: association.firstLineRegExp, mime: mime }); } - if (item === previousItem) { - continue; - } - let letter = item.charAt(0); - if (currentLetter !== letter) { - pushCurrentRow(); - currentLetter = letter; - currentRow = []; + // User configured + if (association.userConfigured && association.pattern) { + userConfiguredTextMimesByFilename[association.pattern] = mime; } - currentRow.push(item); - previousItem = item; - } - pushCurrentRow(); - - return result.join(',\n'); -} - -/** - * Allow to register extra text mimes dynamically based on filename - */ -export function registerTextMimeByFilename(nameOrPatternOrPrefix: string, mime: string): void { - if (nameOrPatternOrPrefix && mime) { - if (registeredTextMimesByFilename[nameOrPatternOrPrefix] && registeredTextMimesByFilename[nameOrPatternOrPrefix] !== mime) { - console.warn('Overwriting filename <<' + nameOrPatternOrPrefix + '>> to now point to mime <<' + mime + '>>'); + // Built in or via Extension + else if (association.pattern) { + if (registeredTextMimesByFilename[association.pattern] && registeredTextMimesByFilename[association.pattern] !== mime) { + console.warn('Overwriting filename <<' + association.pattern + '>> to now point to mime <<' + mime + '>>'); + } + registeredTextMimesByFilename[association.pattern] = mime; } - registeredTextMimesByFilename[nameOrPatternOrPrefix] = mime; } } /** - * Allow to register extra text mimes dynamically based on firstline - */ -export function registerTextMimeByFirstLine(firstLineRegexp: RegExp, mime: string): void { - if (firstLineRegexp && mime) { - registeredTextMimesByFirstLine.push({ regexp: firstLineRegexp, mime: mime }); - } -} - -/** - * Given a comma separated list of mimes in order of priority, find if the list describes a binary - * or textual resource. - */ -export function isBinaryMime(mimes: string): boolean; -export function isBinaryMime(mimes: string[]): boolean; -export function isBinaryMime(mimes: any): boolean { - if (!mimes) { - return false; - } - - let mimeVals: string[]; - if (types.isArray(mimes)) { - mimeVals = (mimes); - } else { - mimeVals = (mimes).split(',').map((mime) => mime.trim()); - } - - return mimeVals.indexOf(MIME_BINARY) >= 0; -} - -/** - * New function for mime type detection supporting application/unknown as concept. + * Given a file, return the best matching mime type for it */ export function guessMimeTypes(path: string, firstLine?: string): string[] { if (!path) { return [MIME_UNKNOWN]; } - // 1.) Firstline gets highest priority - if (firstLine) { - if (strings.startsWithUTF8BOM(firstLine)) { - firstLine = firstLine.substr(1); - } + path = path.toLowerCase(); + let filename = paths.basename(path); - if (firstLine.length > 0) { - for (let i = 0; i < registeredTextMimesByFirstLine.length; ++i) { + // 1.) Configured mappings have highest priority + let configuredMime = guessMimeTypeByFilename(filename, userConfiguredTextMimesByFilename); + if (configuredMime) { + return [configuredMime, MIME_TEXT]; + } - // Make sure the entire line matches, not just a subpart. - let matches = firstLine.match(registeredTextMimesByFirstLine[i].regexp); - if (matches && matches.length > 0 && matches[0].length === firstLine.length) { - return [registeredTextMimesByFirstLine[i].mime, MIME_TEXT]; - } - } + // 2.) Firstline has high priority over registered mappings + if (firstLine) { + let firstlineMime = guessMimeTypeByFirstline(firstLine); + if (firstlineMime) { + return [firstlineMime, MIME_TEXT]; } } - // Check with file name and extension - path = path.toLowerCase(); - let filename = paths.basename(path); + // 3.) Registered mappings have lowest priority + let registeredMime = guessMimeTypeByFilename(filename, registeredTextMimesByFilename); + if (registeredMime) { + return [registeredMime, MIME_TEXT]; + } + return [MIME_UNKNOWN]; +} + +function guessMimeTypeByFilename(filename: string, map: { [str: string]: string; }): string { let exactNameMatch: string; let extensionMatch: string; let patternNameMatch: string; // Check for dynamically registered match based on filename and extension - for (let nameOrPatternOrPrefix in registeredTextMimesByFilename) { + for (let nameOrPatternOrPrefix in map) { let nameOrPatternOrExtensionLower: string = nameOrPatternOrPrefix.toLowerCase(); // First exact name match @@ -172,20 +115,56 @@ export function guessMimeTypes(path: string, firstLine?: string): string[] { // 2.) Exact name match has second highest prio if (exactNameMatch) { - return [registeredTextMimesByFilename[exactNameMatch], MIME_TEXT]; + return map[exactNameMatch]; } // 3.) Match on pattern if (patternNameMatch) { - return [registeredTextMimesByFilename[patternNameMatch], MIME_TEXT]; + return map[patternNameMatch]; } // 4.) Match on extension comes next if (extensionMatch) { - return [registeredTextMimesByFilename[extensionMatch], MIME_TEXT]; + return map[extensionMatch]; } - return [MIME_UNKNOWN]; + return null; +} + +function guessMimeTypeByFirstline(firstLine: string): string { + if (strings.startsWithUTF8BOM(firstLine)) { + firstLine = firstLine.substr(1); + } + + if (firstLine.length > 0) { + for (let i = 0; i < registeredTextMimesByFirstLine.length; ++i) { + + // Make sure the entire line matches, not just a subpart. + let matches = firstLine.match(registeredTextMimesByFirstLine[i].regexp); + if (matches && matches.length > 0 && matches[0].length === firstLine.length) { + return registeredTextMimesByFirstLine[i].mime; + } + } + } + + return null; +} + +export function isBinaryMime(mimes: string): boolean; +export function isBinaryMime(mimes: string[]): boolean; +export function isBinaryMime(mimes: any): boolean { + if (!mimes) { + return false; + } + + let mimeVals: string[]; + if (types.isArray(mimes)) { + mimeVals = (mimes); + } else { + mimeVals = (mimes).split(',').map((mime) => mime.trim()); + } + + return mimeVals.indexOf(MIME_BINARY) >= 0; } export function isUnspecific(mime: string[] | string): boolean { diff --git a/src/vs/base/test/common/mime.test.ts b/src/vs/base/test/common/mime.test.ts index 086526cfd96..e3f2952e312 100644 --- a/src/vs/base/test/common/mime.test.ts +++ b/src/vs/base/test/common/mime.test.ts @@ -5,35 +5,39 @@ 'use strict'; import * as assert from 'assert'; -import { guessMimeTypes, registerTextMimeByFilename, registerTextMimeByFirstLine } from 'vs/base/common/mime'; +import { guessMimeTypes, registerTextMime } from 'vs/base/common/mime'; + +function register(pattern: string, mime: string, user?: boolean): void { + registerTextMime(mime, { pattern: pattern, userConfigured: user }); +} suite('Mime', () => { test('Dynamically Register Text Mime', () => { var guess = guessMimeTypes('foo.monaco'); assert.deepEqual(guess, ['application/unknown']); - registerTextMimeByFilename('.monaco', 'text/monaco'); + register('.monaco', 'text/monaco'); guess = guessMimeTypes('foo.monaco'); assert.deepEqual(guess, ['text/monaco', 'text/plain']); guess = guessMimeTypes('.monaco'); assert.deepEqual(guess, ['text/monaco', 'text/plain']); - registerTextMimeByFilename('Codefile', 'text/code'); + register('Codefile', 'text/code'); guess = guessMimeTypes('Codefile'); assert.deepEqual(guess, ['text/code', 'text/plain']); guess = guessMimeTypes('foo.Codefile'); assert.deepEqual(guess, ['application/unknown']); - registerTextMimeByFilename('Docker*', 'text/docker'); + register('Docker*', 'text/docker'); guess = guessMimeTypes('Docker-debug'); assert.deepEqual(guess, ['text/docker', 'text/plain']); guess = guessMimeTypes('docker-PROD'); assert.deepEqual(guess, ['text/docker', 'text/plain']); - registerTextMimeByFirstLine(/RegexesAreNice/, 'text/nice-regex'); + registerTextMime('text/nice-regex', { firstLineRegExp: /RegexesAreNice/ }); guess = guessMimeTypes('Randomfile.noregistration', 'RegexesAreNice'); assert.deepEqual(guess, ['text/nice-regex', 'text/plain']); @@ -45,8 +49,8 @@ suite('Mime', () => { }); test('Mimes Priority', () => { - registerTextMimeByFilename('.monaco', 'text/monaco'); - registerTextMimeByFirstLine(/foobar/, 'text/foobar'); + register('.monaco', 'text/monaco'); + registerTextMime('text/foobar', { firstLineRegExp: /foobar/ }); var guess = guessMimeTypes('foo.monaco'); assert.deepEqual(guess, ['text/monaco', 'text/plain']); @@ -54,32 +58,32 @@ suite('Mime', () => { guess = guessMimeTypes('foo.monaco', 'foobar'); assert.deepEqual(guess, ['text/foobar', 'text/plain']); - registerTextMimeByFilename('dockerfile', 'text/winner'); - registerTextMimeByFilename('dockerfile*', 'text/looser'); + register('dockerfile', 'text/winner'); + register('dockerfile*', 'text/looser'); guess = guessMimeTypes('dockerfile'); assert.deepEqual(guess, ['text/winner', 'text/plain']); }); test('Specificity priority 1', () => { - registerTextMimeByFilename('.monaco2', 'text/monaco2'); - registerTextMimeByFilename('specific.monaco2', 'text/specific-monaco2'); + register('.monaco2', 'text/monaco2'); + register('specific.monaco2', 'text/specific-monaco2'); assert.deepEqual(guessMimeTypes('specific.monaco2'), ['text/specific-monaco2', 'text/plain']); assert.deepEqual(guessMimeTypes('foo.monaco2'), ['text/monaco2', 'text/plain']); }); test('Specificity priority 2', () => { - registerTextMimeByFilename('specific.monaco3', 'text/specific-monaco3'); - registerTextMimeByFilename('.monaco3', 'text/monaco3'); + register('specific.monaco3', 'text/specific-monaco3'); + register('.monaco3', 'text/monaco3'); assert.deepEqual(guessMimeTypes('specific.monaco3'), ['text/specific-monaco3', 'text/plain']); assert.deepEqual(guessMimeTypes('foo.monaco3'), ['text/monaco3', 'text/plain']); }); test('Mimes Priority - Longest Extension wins', () => { - registerTextMimeByFilename('.monaco', 'text/monaco'); - registerTextMimeByFilename('.monaco.xml', 'text/monaco-xml'); - registerTextMimeByFilename('.monaco.xml.build', 'text/monaco-xml-build'); + register('.monaco', 'text/monaco'); + register('.monaco.xml', 'text/monaco-xml'); + register('.monaco.xml.build', 'text/monaco-xml-build'); var guess = guessMimeTypes('foo.monaco'); assert.deepEqual(guess, ['text/monaco', 'text/plain']); @@ -90,4 +94,12 @@ suite('Mime', () => { guess = guessMimeTypes('foo.monaco.xml.build'); assert.deepEqual(guess, ['text/monaco-xml-build', 'text/plain']); }); + + test('Mimes Priority - User configured wins', () => { + register('.monaco.xml', 'text/monaco', true); + register('.monaco.xml', 'text/monaco-xml'); + + var guess = guessMimeTypes('foo.monaco.xml'); + assert.deepEqual(guess, ['text/monaco', 'text/plain']); + }); }); diff --git a/src/vs/base/test/node/mime/mime.test.ts b/src/vs/base/test/node/mime/mime.test.ts index 92a5268cc47..a1a4132303a 100644 --- a/src/vs/base/test/node/mime/mime.test.ts +++ b/src/vs/base/test/node/mime/mime.test.ts @@ -6,7 +6,6 @@ 'use strict'; import assert = require('assert'); -import path = require('path'); import mimeCommon = require('vs/base/common/mime'); import mime = require('vs/base/node/mime'); @@ -24,7 +23,7 @@ suite('Mime', () => { }); test('detectMimesFromFile (PNG saved as TXT)', function(done: () => void) { - mimeCommon.registerTextMimeByFilename('.txt', 'text/plain'); + mimeCommon.registerTextMime('text/plain', { pattern: '.txt' }); var file = require.toUrl('./fixtures/some.png.txt'); mime.detectMimesFromFile(file, (error, mimes) => { assert.equal(error, null); diff --git a/src/vs/editor/common/services/languagesRegistry.ts b/src/vs/editor/common/services/languagesRegistry.ts index 378b222c23a..97a0ac24c55 100644 --- a/src/vs/editor/common/services/languagesRegistry.ts +++ b/src/vs/editor/common/services/languagesRegistry.ts @@ -103,19 +103,19 @@ export class LanguagesRegistry { if (Array.isArray(lang.extensions)) { for (let extension of lang.extensions) { - mime.registerTextMimeByFilename(extension, primaryMime); + mime.registerTextMime(primaryMime, { pattern: extension }); } } if (Array.isArray(lang.filenames)) { for (let filename of lang.filenames) { - mime.registerTextMimeByFilename(filename, primaryMime); + mime.registerTextMime(primaryMime, { pattern: filename }); } } if (Array.isArray(lang.filenamePatterns)) { for (let filenamePattern of lang.filenamePatterns) { - mime.registerTextMimeByFilename(filenamePattern, primaryMime); + mime.registerTextMime(primaryMime, { pattern: filenamePattern }); } } @@ -127,7 +127,7 @@ export class LanguagesRegistry { try { var firstLineRegex = new RegExp(firstLineRegexStr); if (!strings.regExpLeadsToEndlessLoop(firstLineRegex)) { - mime.registerTextMimeByFirstLine(firstLineRegex, primaryMime); + mime.registerTextMime(primaryMime, { firstLineRegExp: firstLineRegex }); } } catch (err) { // Most likely, the regex was bad diff --git a/src/vs/workbench/parts/files/common/editors/fileAssociations.ts b/src/vs/workbench/parts/files/common/editors/fileAssociations.ts index ce0be2b9fee..8f26f7033a2 100644 --- a/src/vs/workbench/parts/files/common/editors/fileAssociations.ts +++ b/src/vs/workbench/parts/files/common/editors/fileAssociations.ts @@ -41,7 +41,7 @@ export class FileAssociations implements IWorkbenchContribution { private onConfigurationChange(configuration: IFilesConfiguration): void { if (configuration.files && configuration.files.associations) { Object.keys(configuration.files.associations).forEach(pattern => { - mime.registerTextMimeByFilename(pattern, this.modeService.getMimeForMode(configuration.files.associations[pattern])); + mime.registerTextMime(this.modeService.getMimeForMode(configuration.files.associations[pattern]), { pattern: pattern, userConfigured: true }); }); } } -- GitLab