Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ability to create custom editors for files #229

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions demo/src/features/customView.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { IDialogService } from 'vscode/services'
import { IDialogService, EditorInput } from 'vscode/services'
import { registerCustomView, registerEditorPane, ViewContainerLocation } from '@codingame/monaco-vscode-views-service-override'
import * as monaco from 'monaco-editor'
import iconUrl from '../Visual_Studio_Code_1.35_icon.svg?url'
import { ServicesAccessor } from 'vscode/vscode/vs/platform/instantiation/common/instantiation'

registerCustomView({
id: 'custom-view',
Expand Down Expand Up @@ -43,7 +44,7 @@ registerCustomView({
}]
})

const { CustomEditorInput } = registerEditorPane({
const { CustomEditorInput, registerEditor } = registerEditorPane({
id: 'custom-editor-pane',
name: 'Custom editor pane',
renderBody (container) {
Expand All @@ -54,9 +55,19 @@ const { CustomEditorInput } = registerEditorPane({

return {
dispose () {
},

async setInput (_accessor: ServicesAccessor, input: EditorInput) {
if (input.resource != null) {
container.innerHTML = 'Opened file: ' + input.resource.path
} else {
container.innerHTML = 'This is a custom editor pane<br />You can render anything you want here'
}
}
}
}
})

registerEditor('*.customeditor')

export { CustomEditorInput }
4 changes: 4 additions & 0 deletions demo/src/features/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ $$
$$`
))

fileSystemProvider.registerFile(new RegisteredMemoryFile(vscode.Uri.file('/tmp/test.customeditor'), `
Custom Editor!`
))

fileSystemProvider.registerFile(new RegisteredMemoryFile(vscode.Uri.file('/tmp/test.css'), `
h1 {
color: DeepSkyBlue;
Expand Down
104 changes: 88 additions & 16 deletions src/service-override/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,19 @@ import 'vs/workbench/contrib/languageDetection/browser/languageDetection.contrib
import 'vs/workbench/contrib/files/browser/files.contribution.js?include=registerConfiguration'
import 'vs/workbench/contrib/files/browser/files.contribution.js?exclude=registerConfiguration'
import { Codicon } from 'vs/base/common/codicons'
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'
import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorDropService'
import { IEditorDropTargetDelegate } from 'vs/workbench/browser/parts/editor/editorDropTarget'
import { IEditorService } from 'vs/workbench/services/editor/common/editorService'
import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'
import { IEditorResolverService, RegisteredEditorInfo, RegisteredEditorOptions, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'
import { EditorResolverService } from 'vs/workbench/services/editor/browser/editorResolverService'
import { BreadcrumbsService, IBreadcrumbsService } from 'vs/workbench/browser/parts/editor/breadcrumbs'
import { IContextViewService } from 'vs/platform/contextview/browser/contextView'
import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'
import { EditorInput, IEditorCloseHandler } from 'vs/workbench/common/editor/editorInput'
import { EditorExtensions, Verbosity } from 'vs/workbench/common/editor'
import { IEditorOptions } from 'vs/platform/editor/common/editor'
import { EditorExtensions, IEditorOpenContext, Verbosity } from 'vs/workbench/common/editor'
import { IEditorOptions, IResourceEditorInput, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'
import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'
import { ITextEditorService, TextEditorService } from 'vs/workbench/services/textfile/common/textEditorService'
import { CodeEditorService } from 'vs/workbench/services/editor/browser/codeEditorService'
Expand Down Expand Up @@ -87,14 +87,15 @@ import { ConfirmResult } from 'vs/platform/dialogs/common/dialogs'
import { ILayoutService } from 'vs/platform/layout/browser/layoutService'
import { IBannerService } from 'vs/workbench/services/banner/browser/bannerService'
import { ITitleService } from 'vs/workbench/services/title/common/titleService'
import { CancellationToken } from 'vs/base/common/cancellation'
import { MonacoDelegateEditorGroupsService, MonacoEditorService, OpenEditor } from './tools/editor'
import getBulkEditServiceOverride from './bulkEdit'
import getLayoutServiceOverride, { LayoutService } from './layout'
import getQuickAccessOverride from './quickaccess'
import getKeybindingsOverride from './keybindings'
import { changeUrlDomain } from './tools/url'
import { registerAssets } from '../assets'
import { registerServiceInitializePostParticipant } from '../lifecycle'
import { registerServiceInitializePostParticipant, serviceInitializedBarrier } from '../lifecycle'

function createPart (id: string, role: string, classes: string[]): HTMLElement {
const part = document.createElement(role === 'status' ? 'footer' /* Use footer element for status bar #98376 */ : 'div')
Expand Down Expand Up @@ -189,6 +190,10 @@ function renderStatusBarPart (container: HTMLElement): IDisposable {
return attachPart(Parts.STATUSBAR_PART, container)
}

interface BodyRenderer extends IDisposable {
setInput? (accessor: ServicesAccessor, input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void>
}

type Label = string | {
short: string
medium: string
Expand All @@ -197,7 +202,7 @@ type Label = string | {
interface EditorPanelOption {
readonly id: string
name: string
renderBody (container: HTMLElement): IDisposable
renderBody (container: HTMLElement): BodyRenderer
CGNonofr marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really happy with extending IDisposable here

an alternative that is used a lot inside VSCode itself is to provide a DisposableStore as the last parameter

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The risk I see with that is that it's easy to forget for the consumer. If they forget to handle that parameter, then it won't get disposed. Now the type system will give an error if dispose() is not implemented on the returned value, making sure that things get cleaned up when we get rid of them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dispose function being mandatory in on the cons column for me 😄 most of the time, you don't need it (for instance if you render a react component using portal?)

Copy link
Collaborator Author

@CompuIves CompuIves Nov 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our React case we do need to handle dispose, because when dispose is called we need to remove the component from our list. Otherwise the component will try to render on an element that is not attached to the DOM anymore. In React we also need to run the disposers of useEffect during that moment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an example, in our React case I actually realised that I forgot to properly dispose when I shared the code with you, because the dispose body was empty. I've since changed the code to this:

image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a last point, I think that this can feel like VSCode, with how they often use classes for UI. E.g. you could also pass this to it:

class MarkdownRenderer extends Disposable {
  constructor() { }
  
  setInput(input) {
    this.draw();
  }
  
  private draw() { /* ... */ }
}

}

interface SimpleEditorInput extends EditorInput {
Expand All @@ -207,13 +212,25 @@ interface SimpleEditorInput extends EditorInput {
setDirty (dirty: boolean): void
}

function registerEditorPane (options: EditorPanelOption): { disposable: IDisposable, CustomEditorInput: new (closeHandler?: IEditorCloseHandler) => SimpleEditorInput } {
type RegisteredEditorInfoWithoutId = Partial<Omit<RegisteredEditorInfo, 'id'>>
type RegisterEditorPaneResult = {
disposable: IDisposable
CustomEditorInput: new (closeHandler?: IEditorCloseHandler, baseInput?: IResourceEditorInput | ITextResourceEditorInput) => SimpleEditorInput
/**
* Allows you to register the editor for a certain file type. When opening that input, it will render this editor.
*/
registerEditor(globPattern: string, info?: RegisteredEditorInfoWithoutId, options?: RegisteredEditorOptions, closeHandler?: IEditorCloseHandler): IDisposable
}

function registerEditorPane (options: EditorPanelOption): RegisterEditorPaneResult {
class CustomEditorPane extends EditorPane {
private content?: HTMLElement
private bodyRenderer?: BodyRenderer
constructor (
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService
@IStorageService storageService: IStorageService,
@IInstantiationService private instantiationService: IInstantiationService
) {
super(options.id, telemetryService, themeService, storageService)
}
Expand All @@ -223,13 +240,31 @@ function registerEditorPane (options: EditorPanelOption): { disposable: IDisposa
this.content.style.display = 'flex'
this.content.style.alignItems = 'stretch'
append(parent, this.content)
this._register(options.renderBody(this.content))
this.bodyRenderer = options.renderBody(this.content)
this._register(this.bodyRenderer)
}

override layout (dimension: Dimension): void {
this.content!.style.height = `${dimension.height}px`
this.content!.style.width = `${dimension.width}px`
}

override async setInput (input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
await this.instantiationService.invokeFunction(accessor => {
if (this.bodyRenderer != null && this.bodyRenderer.setInput != null) {
return this.bodyRenderer.setInput(
accessor,
input,
options,
context,
token
)
}
return undefined
})

return super.setInput(input, options, context, token)
}
}

class CustomEditorInput extends EditorInput implements SimpleEditorInput {
Expand All @@ -240,7 +275,7 @@ function registerEditorPane (options: EditorPanelOption): { disposable: IDisposa
private description: Label = options.name
private dirty: boolean = false

constructor (public override readonly closeHandler?: IEditorCloseHandler) {
constructor (public override readonly closeHandler?: IEditorCloseHandler, public baseInput?: IResourceEditorInput | ITextResourceEditorInput) {
super()
}

Expand All @@ -249,7 +284,7 @@ function registerEditorPane (options: EditorPanelOption): { disposable: IDisposa
}

override get resource (): URI | undefined {
return undefined
return this.baseInput?.resource
}

public setName (name: string) {
Expand Down Expand Up @@ -304,17 +339,48 @@ function registerEditorPane (options: EditorPanelOption): { disposable: IDisposa
}
}

const disposable = Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane(
const disposableStore = new DisposableStore()

disposableStore.add(Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane(
EditorPaneDescriptor.create(
CustomEditorPane,
options.id,
options.name
),
[new SyncDescriptor(CustomEditorInput)])
[new SyncDescriptor(CustomEditorInput)]))

return {
disposable,
CustomEditorInput
disposable: disposableStore,
CustomEditorInput,
registerEditor (globPattern, registerEditorInfo = {}, registeredEditorOptions = {}, closeHandler): IDisposable {
if (!serviceInitializedBarrier.isOpen()) {
throw new Error("Can't register editor before services are initialized")
}

const resolverService = StandaloneServices.get(IEditorResolverService)
const registeredDisposable = resolverService.registerEditor(
globPattern,
{
id: CustomEditorInput.ID,
label: options.name,
priority: RegisteredEditorPriority.default,
...registerEditorInfo
},
registeredEditorOptions,
{
createEditorInput (editorInput: IResourceEditorInput | ITextResourceEditorInput, _group: IEditorGroup) {
return {
options: {},
editor: new CustomEditorInput(closeHandler, editorInput)
}
}
}
)

disposableStore.add(registeredDisposable)

return registeredDisposable
}
}
}

Expand Down Expand Up @@ -652,6 +718,10 @@ export {
IEditorCloseHandler,
ConfirmResult,
registerEditorPane,
RegisterEditorPaneResult,
RegisteredEditorInfoWithoutId as RegisterEditorOptions,
RegisteredEditorInfo,
RegisteredEditorOptions,

renderPart,
renderSidebarPart,
Expand All @@ -676,5 +746,7 @@ export {
SidebarPart,
ActivitybarPart,
PanelPart,
Parts
Parts,

BodyRenderer
}