18.1.0 • Published 8 months ago

@cisstech/nge-ide v18.1.0

Weekly downloads
-
License
MIT
Repository
github
Last release
8 months ago

An extensible and flexible open source ide written in Angular.

Tests codecov codefactor GitHub Tag npm package NPM downloads licence code style: prettier

📄 Docs

Documentation available at https://cisstech.github.io/nge-ide/

📦 Installation

npm install @cisstech/nge @cisstech/nge-ide monaco-editor marked

🚀 Quick Start

Basic Integration

Add the IDE to your Angular application by importing the NgeIdeModule in your app module:

import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'

import { NgeIdeModule } from '@cisstech/nge-ide'
import { NgeIdeExplorerModule } from '@cisstech/nge-ide/explorer'
import { NgeIdeSearchModule } from '@cisstech/nge-ide/search'
import { NgeIdeSettingsModule } from '@cisstech/nge-ide/settings'

import { NgeIdeProblemsModule } from '@cisstech/nge-ide/problems'
import { NgeIdeNotificationsModule } from '@cisstech/nge-ide/notifications'


@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule

    NgeIdeModule,
    NgeIdeExplorerModule,
    NgeIdeSearchModule,
    NgeIdeSettingsModule,

    NgeIdeProblemsModule,
    NgeIdeNotificationsModule,
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Component Usage

Add the IDE root component to your template:

<ide-root />
import { Component, OnDestroy, OnInit } from '@angular/core'
import { FileService, IdeService, MemFileProvider } from '@cisstech/nge-ide/core'
import { Subscription } from 'rxjs'

@Component({
  selector: 'app-component',
  templateUrl: 'component.component.html',
  styleUrls: ['component.component.scss'],
})
export class AppComponent implements OnInit, OnDestroy {
  private subscription?: Subscription

  constructor(
    private readonly ide: IdeService,
    private readonly fileService: FileService
  ) {}

  ngOnInit() {
    this.subscription = this.ide.onAfterStart(() => {
      const provider = new MemFileProvider()
      this.fileService.registerProvider(provider)
      provider.roots.forEach((root) => {
        this.fileService.registerFolders({
          name: root.authority,
          uri: root,
        })
      })
    })
  }

  ngOnDestroy(): void {
    this.subscription?.unsubscribe()
  }
}

This basic setup will display the IDE with the default in memory file system and the optional contributions :

  • NgeIdeExplorerModule : Contribution to navigate the IDE files from the sidebar
  • NgeIdeSearchModule : Contribution to enable search view
  • NgeIdeSettingsModule: Contribution to enable settings view
  • NgeIdeProblemsModule : Contribution to enable problems view in the bottom area.
  • NgeIdeNotificationsModule : Contribution to enable notifications view in the bottom area.

Basic File System Setup

In this example configuration, the IDE use an in-memory file system, which is not what you want. You can add your own file system by implementing the IFileSystemProvider interface and registering it with the FileService.

import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { FileSystemProvider, FileSystemProviderCapabilities, IFile, FileSystemError } from '@cisstech/nge-ide/core'
import { firstValueFrom } from 'rxjs'

@Injectable()
export class HttpFileSystemProvider extends FileSystemProvider {
  readonly scheme = 'http' // URI scheme for this provider
  readonly capabilities = FileSystemProviderCapabilities.FileRead // Read-only capabilities for this example

  constructor(private readonly http: HttpClient) {
    super()
  }

  // Check if provider has a specific capability
  hasCapability(capability: FileSystemProviderCapabilities): boolean {
    return (this.capabilities & capability) !== 0
  }

  // Implement read method to fetch file content via HTTP
  async read(uri: monaco.Uri): Promise<string> {
    try {
      // Convert IDE URI to actual HTTP URL
      const url = `https://api.myservice.com/files${uri.path}`
      const response = await firstValueFrom(this.http.get(url, { responseType: 'text' }))
      return response
    } catch (error) {
      throw FileSystemError.FileNotFound(uri)
    }
  }

  // Implement stat method to get file metadata
  async stat(uri: monaco.Uri): Promise<IFile> {
    try {
      const url = `https://api.myservice.com/files/stat${uri.path}`
      const response = await firstValueFrom(
        this.http.get<{
          isDirectory: boolean
          name: string
          path: string
        }>(url)
      )

      return {
        uri: uri,
        isFolder: response.isDirectory,
        readOnly: true, // Read-only in this example
        name: response.name,
      }
    } catch (error) {
      throw FileSystemError.FileNotFound(uri)
    }
  }

  // Implement directory listing
  async readDirectory(uri: monaco.Uri): Promise<IFile[]> {
    try {
      const url = `https://api.myservice.com/files/list${uri.path}`
      const response = await firstValueFrom(
        this.http.get<
          Array<{
            isDirectory: boolean
            name: string
            path: string
          }>
        >(url)
      )

      return response.map((item) => ({
        uri: monaco.Uri.parse(`${this.scheme}://${uri.authority}${item.path}`),
        isFolder: item.isDirectory,
        readOnly: true,
        name: item.name,
      }))
    } catch (error) {
      throw FileSystemError.FileNotFound(uri)
    }
  }

  // Implement other methods as needed based on your capabilities
  // write, delete, rename, etc.
}

// Register the provider in a contribution
@Injectable()
export class FileSystemContribution implements IContribution {
  readonly id = 'app.filesystem-contribution'

  constructor(
    private readonly fileService: FileService,
    private readonly httpFileSystemProvider: HttpFileSystemProvider
  ) {}

  activate() {
    // Register the provider
    this.fileService.registerProvider(this.httpFileSystemProvider)

    // Register root folders
    this.fileService.registerFolders([
      {
        name: 'My API Files',
        uri: monaco.Uri.parse('http://api/'),
      },
      {
        name: 'My Documentation',
        uri: monaco.Uri.parse('http://docs/'),
      },
    ])
  }
}

@NgModule({
  providers: [HttpFileSystemProvider, { provide: CONTRIBUTION, multi: true, useClass: FileSystemContribution }],
})
export class AppFileSystemModule {}

The example above demonstrates how to implement a simple HTTP-based file system provider. When implementing your own provider, you should:

  1. Extend the FileSystemProvider class and specify a unique scheme
  2. Define which capabilities your provider supports (read, write, delete, etc.)
  3. Implement the required methods based on your provider's capabilities
  4. Register the provider with the FileService in a contribution
  5. Register root folders to make them visible in the file explorer

For a complete implementation, you may need to support additional capabilities like file searching, writing, deleting, etc., based on your application's requirements.

⌨️ Development

  • Clone and install
git clone https://github.com/cisstech/nge-ide
cd nge-ide
npm install
  • Serve demo
npm run start

Browser would open automatically at http://localhost:4200/.

💡 Contribution System Guide

NGE IDE is built around a powerful contribution-based architecture that allows you to enhance and extend the IDE functionality. This guide walks you through understanding and building your own contributions.

Understanding Contributions

In NGE IDE, a "contribution" is any component, service, or feature that extends the IDE's functionality. Contributions follow a common interface:

export interface IContribution {
  readonly id: string; // Unique identifier
  activate?(injector: Injector): void or Promise<void>; // Setup code
  deactivate?(): void or Promise<void>; // Cleanup code
}

Contributions are registered using Angular's dependency injection system:

@NgModule({
  providers: [{ provide: CONTRIBUTION, multi: true, useClass: MyContribution }],
})
export class MyModule {}

Step 1: Adding Core Contributions

Start with the essential built-in contributions that provide basic IDE functionality:

import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'

import { NgeIdeModule } from '@cisstech/nge-ide'
import { NgeIdeExplorerModule } from '@cisstech/nge-ide/explorer' // File explorer
import { NgeIdeSearchModule } from '@cisstech/nge-ide/search' // Search functionality
import { NgeIdeSettingsModule } from '@cisstech/nge-ide/settings' // Settings UI

@NgModule({
  imports: [BrowserModule, NgeIdeModule, NgeIdeExplorerModule, NgeIdeSearchModule, NgeIdeSettingsModule],
  bootstrap: [AppComponent],
})
export class AppModule {}

This basic setup gives you:

  • A fully functional IDE layout with sidebars and panels
  • File explorer for navigating your files
  • Search capabilities across your codebase
  • Settings management interface

Step 2: Adding File System Support

Next, create a custom file system provider to access your application's files:

@Injectable()
export class MyFileSystemContribution implements IContribution {
  readonly id = 'app.file-system'

  constructor(
    private readonly fileService: FileService,
    private readonly httpFs: HttpFileSystemProvider
  ) {}

  activate() {
    // Register the file provider
    this.fileService.registerProvider(this.httpFs)

    // Register root folders to appear in the explorer
    this.fileService.registerFolders({
      name: 'Project Files',
      uri: monaco.Uri.parse('http://my-project/'),
    })
  }
}

The FileService exposes these key APIs:

  • registerProvider(provider): Register a file system provider
  • registerFolders(folders): Register root folders to show in explorer
  • open(uri), save(uri), close(uri): File operations
  • find(uri), findChildren(file): Navigation methods
  • isDirty(uri): Check if file has unsaved changes

Step 3: Adding View Containers and Views

View containers group related views in the IDE sidebars:

@Injectable()
export class MyViewContribution implements IContribution {
  readonly id = 'app.my-views'

  activate(injector: Injector) {
    const viewContainerService = injector.get(ViewContainerService)
    const viewService = injector.get(ViewService)

    // 1. Register a container in the sidebar
    viewContainerService.register(
      new (class extends SidebarContainer {
        readonly id = 'app.my-container'
        readonly title = 'My Tools'
        readonly icon = new CodIcon('tools')
        readonly side = 'left' // 'left' or 'right'
        readonly align = 'top' // 'top' or 'bottom'
      })()
    )

    // 2. Register views inside the container
    viewService.register({
      id: 'app.my-view',
      title: 'My View',
      viewContainerId: 'app.my-container',
      component: () => MyViewComponent,
    })
  }
}

Key view APIs:

  • ViewContainerService: Manages containers (sidebars, panels)
  • ViewService: Manages individual views within containers
  • Built-in containers: leftSideBar, rightSideBar, bottomPanel

Step 4: Adding Commands

Commands are actions that can be triggered by users through menus, buttons, keyboard shortcuts or api calls to command service:

@Injectable()
export class MyCommandContribution implements IContribution {
  readonly id = 'app.my-commands'

  constructor(
    private readonly commandService: CommandService,
    private readonly toolbarService: ToolbarService
  ) {}

  activate() {
    // 1. Create and register a command
    const myCommand: ICommand = {
      id: 'app.myCommand',
      label: 'Do Something',
      icon: new CodIcon('zap'),
      get enabled() {
        return true
      },
      execute: () => {
        console.log('Command executed!')
      },
    }

    this.commandService.register(myCommand)

    // 2. Add the command to the editor toolbar
    this.toolbarService.register(
      new ToolbarButton({
        group: ToolbarGroups.EDIT, // Predefined toolbar groups
        command: myCommand,
        priority: 10,
      })
    )
  }
}

Command system APIs:

  • CommandService: Register and execute commands
  • ToolbarService: Add buttons to editor toolbars
  • KeyBindService: Register keyboard shortcuts
  • Predefined toolbar groups: FILE, EDIT, SELECTION, VIEW, GO
  • Use command as angular service to access to dependency injection in the constructor.

Step 5: Editor Enhancements

Enhance the editor with custom functionality:

@Injectable()
export class MyEditorContribution implements IContribution {
  readonly id = 'app.editor-contrib'

  constructor(
    private readonly editorService: EditorService,
    private readonly monacoService: MonacoService,
    private readonly previewService: PreviewService
  ) {}

  activate() {
    // 1. Register custom file editors
    this.editorService.registerEditors(new MyCustomEditor())

    // 2. Register custom preview handlers for files
    this.previewService.register(new ImagePreviewHandler())

    // 3. Listen for editor events
    this.monacoService.onDidCreateEditor.subscribe((editor) => {
      // Configure the Monaco editor instance
      editor.updateOptions({ lineNumbers: 'on' })
    })

    // 4. Register editor commands
    this.editorService.registerCommands(MyEditorCommandClass)
  }
}

Editor APIs:

  • EditorService: Manage editors and editor groups
  • MonacoService: Direct access to Monaco editor instances
  • PreviewService: Register preview handlers for files

Step 6: Other API Services

NGE IDE provides several other services for building rich IDE features:

@Injectable()
export class MyAdvancedContribution implements IContribution {
  readonly id = 'app.advanced'

  constructor(
    private readonly notificationService: NotificationService,
    private readonly dialogService: DialogService,
    private readonly statusBarService: StatusBarService,
    private readonly taskService: TaskService,
    private readonly diagnosticService: DiagnosticService,
    private readonly settingsService: SettingsService
  ) {}

  activate() {
    // Show notifications
    this.notificationService.publishInfo('IDE initialized')

    // Add item to status bar
    this.statusBarService.register({
      id: 'app.status.item',
      side: 'right',
      priority: 10,
      content: 'Ready',
      tooltip: 'System status',
    })

    // Report diagnostics for a file
    const uri = monaco.Uri.parse('file:///myfile.ts')
    this.diagnosticService.setDiagnostics(uri, [
      {
        message: 'Variable not used',
        severity: DiagnosticSeverity.Warning,
        range: {
          start: { lineNumber: 5, column: 10 },
          end: { lineNumber: 5, column: 15 },
        },
      },
    ])

    // Run background task with progress
    const task = this.taskService.run('Processing files')
    setTimeout(() => task.end(), 3000)

    // Access or modify settings
    this.settingsService.set('editor', 'fontSize', 14)
  }
}

Additional available services:

  • NotificationService: Show notifications to users
  • DialogService: Display modal dialogs
  • StatusBarService: Add items to the status bar
  • TaskService: Run and track background tasks
  • DiagnosticService: Report problems in files
  • SettingsService: Manage user/workspace settings

Putting It All Together

A complete module that contributes a file system, views, and commands:

@NgModule({
  imports: [NgeIdeModule, NgeIdeExplorerModule, NgeIdeSearchModule],
  declarations: [MyViewComponent],
  providers: [
    MyFileSystemProvider,
    { provide: CONTRIBUTION, multi: true, useClass: MyFileSystemContribution },
    { provide: CONTRIBUTION, multi: true, useClass: MyViewContribution },
    { provide: CONTRIBUTION, multi: true, useClass: MyCommandContribution },
    { provide: CONTRIBUTION, multi: true, useClass: MyEditorContribution },
  ],
})
export class MyIDEFeatureModule {}

This progressive approach allows you to build up your IDE's functionality layer by layer, from basic file system access to rich editor features, all through the unified contribution system.

🤝 Contribution

Contributions are always welcome.

Please read our CONTRIBUTING.md first. You can submit any ideas as pull requests or as GitHub issues.

Please just make sure that ...

Your code style matches with the rest of the project

Unit tests pass

Linter passes

❓ Support Development

The use of this library is totally free.

As the owner and primary maintainer of this project, I am putting a lot of time and effort beside my job, my family and my private time to bring the best support I can by answering questions, addressing issues and improving the library to provide more and more features over time.

If this project has been useful, that it helped you or your business to save precious time, don't hesitate to give it a star to support its maintenance and future development.

✨ License

MIT © Mamadou Cisse

18.1.0

8 months ago

18.0.1

9 months ago

18.0.0

1 year ago

17.7.4

1 year ago

17.7.3

1 year ago

17.7.2

1 year ago

17.7.1

1 year ago

17.7.0

2 years ago

17.6.0

2 years ago

17.5.5

2 years ago

17.5.4

2 years ago

17.5.0

2 years ago

17.4.0

2 years ago

17.5.2

2 years ago

17.5.1

2 years ago

17.5.3

2 years ago

17.1.0

2 years ago

17.0.0

2 years ago

15.2.0

2 years ago

15.2.3

2 years ago

16.0.0

2 years ago

15.1.3

2 years ago

15.1.2

2 years ago

15.1.1

2 years ago

15.1.0

3 years ago

15.0.1

3 years ago

13.0.4

3 years ago

15.0.0

3 years ago

13.0.3

4 years ago