diff --git a/config/default_config.nim b/config/default_config.nim index 69ce4af6..6541b81e 100644 --- a/config/default_config.nim +++ b/config/default_config.nim @@ -109,11 +109,13 @@ proc loadDefaultKeybindings*(clearExisting: bool = false) {.expose("load-default addCommand "editor", "gl", "choose-location" addCommand "editor", "gg", "choose-git-active-files", false addCommand "editor", "GG", "choose-git-active-files", true - addCommand "editor", "ge", "explore-files" - addCommand "editor", "gw", "explore-workspace-primary" - addCommand "editor", "gu", "explore-user-config-dir" - addCommand "editor", "ga", "explore-app-config-dir" - addCommand "editor", "gh", "explore-help" + addCommand "editor", "ge", "explore-files", "" + addCommand "editor", "gv", "explore-files", "", showVFS=true + addCommand "editor", "gw", "explore-files", "ws0://" + addCommand "editor", "gW", "explore-files", "ws0://".normalizePath + addCommand "editor", "gu", "explore-files", "home://.nev" + addCommand "editor", "ga", "explore-files", "app://config" + addCommand "editor", "gh", "explore-files", "app://docs" addCommand "editor", "gp", "explore-current-file-directory" addCommand "editor", "gs", "search-global-interactive" addCommand "editor", "gk", "browse-keybinds" @@ -183,6 +185,7 @@ proc loadDefaultKeybindings*(clearExisting: bool = false) {.expose("load-default addCommand "popup.selector.git", "", "revert-selected" addCommand "popup.selector.file-explorer", "", "go-up" addCommand "popup.selector.file-explorer", "", "go-up" + addCommand "popup.selector.file-explorer", "", "enter-normalized" addCommandBlock "editor", "al": runLastConfiguration() diff --git a/config/wasm/harpoon.wasm b/config/wasm/harpoon.wasm index 2716f109..dd5947d7 100644 Binary files a/config/wasm/harpoon.wasm and b/config/wasm/harpoon.wasm differ diff --git a/config/wasm/keybindings_plugin.wasm b/config/wasm/keybindings_plugin.wasm index d437ca6f..01b1fd89 100644 Binary files a/config/wasm/keybindings_plugin.wasm and b/config/wasm/keybindings_plugin.wasm differ diff --git a/config/wasm/vscode_config_plugin.wasm b/config/wasm/vscode_config_plugin.wasm index 5ec89512..ca092a95 100644 Binary files a/config/wasm/vscode_config_plugin.wasm and b/config/wasm/vscode_config_plugin.wasm differ diff --git a/scripting/editor_api_wasm.nim b/scripting/editor_api_wasm.nim index 32287255..464cec26 100644 --- a/scripting/editor_api_wasm.nim +++ b/scripting/editor_api_wasm.nim @@ -558,38 +558,19 @@ proc installTreesitterParser*(language: string; host: string = "github.com") {. argsJsonString.cstring) -proc editor_exploreFiles_void_App_string_wasm(arg: cstring): cstring {.importc.} -proc exploreFiles*(root: string = "") {.gcsafe, raises: [].} = +proc editor_exploreFiles_void_App_string_bool_bool_wasm(arg: cstring): cstring {. + importc.} +proc exploreFiles*(root: string = ""; showVFS: bool = false; + normalize: bool = true) {.gcsafe, raises: [].} = var argsJson = newJArray() argsJson.add root.toJson() + argsJson.add showVFS.toJson() + argsJson.add normalize.toJson() let argsJsonString = $argsJson - let res {.used.} = editor_exploreFiles_void_App_string_wasm( - argsJsonString.cstring) - - -proc editor_exploreUserConfigDir_void_App_wasm(arg: cstring): cstring {.importc.} -proc exploreUserConfigDir*() {.gcsafe, raises: [].} = - var argsJson = newJArray() - let argsJsonString = $argsJson - let res {.used.} = editor_exploreUserConfigDir_void_App_wasm( + let res {.used.} = editor_exploreFiles_void_App_string_bool_bool_wasm( argsJsonString.cstring) -proc editor_exploreAppConfigDir_void_App_wasm(arg: cstring): cstring {.importc.} -proc exploreAppConfigDir*() {.gcsafe, raises: [].} = - var argsJson = newJArray() - let argsJsonString = $argsJson - let res {.used.} = editor_exploreAppConfigDir_void_App_wasm( - argsJsonString.cstring) - - -proc editor_exploreHelp_void_App_wasm(arg: cstring): cstring {.importc.} -proc exploreHelp*() {.gcsafe, raises: [].} = - var argsJson = newJArray() - let argsJsonString = $argsJson - let res {.used.} = editor_exploreHelp_void_App_wasm(argsJsonString.cstring) - - proc editor_exploreWorkspacePrimary_void_App_wasm(arg: cstring): cstring {. importc.} proc exploreWorkspacePrimary*() {.gcsafe, raises: [].} = diff --git a/scripting/vfs_api_wasm.nim b/scripting/vfs_api_wasm.nim index 47f96cdb..88e37fe9 100644 --- a/scripting/vfs_api_wasm.nim +++ b/scripting/vfs_api_wasm.nim @@ -17,6 +17,20 @@ proc mountVfs*(parentPath: string; prefix: string; config: JsonNode) {.gcsafe, argsJsonString.cstring) +proc vfs_normalizePath_string_VFSService_string_wasm(arg: cstring): cstring {. + importc.} +proc normalizePath*(path: string): string {.gcsafe, raises: [].} = + var argsJson = newJArray() + argsJson.add path.toJson() + let argsJsonString = $argsJson + let res {.used.} = vfs_normalizePath_string_VFSService_string_wasm( + argsJsonString.cstring) + try: + result = parseJson($res).jsonTo(typeof(result)) + except: + raiseAssert(getCurrentExceptionMsg()) + + proc vfs_dumpVfsHierarchy_void_VFSService_wasm(arg: cstring): cstring {.importc.} proc dumpVfsHierarchy*() {.gcsafe, raises: [].} = var argsJson = newJArray() diff --git a/src/app.nim b/src/app.nim index 9176cdcd..30c288e2 100644 --- a/src/app.nim +++ b/src/app.nim @@ -1,6 +1,6 @@ import std/[sequtils, strformat, strutils, tables, options, os, json, macros, sugar, streams, deques] import misc/[id, util, timer, event, myjsonutils, traits, rect_utils, custom_logger, custom_async, - array_set, delayed_task, disposable_ref] + array_set, delayed_task, disposable_ref, regex] import ui/node import scripting/[expose, scripting_base] import platform/[platform] @@ -32,6 +32,9 @@ logCategory "app" const configDirName = "." & appName const defaultSessionName = &".{appName}-session" +const appConfigDir = "app://config" +const homeConfigDir = "home://" & configDirName +const workspaceConfigDir = "ws0://" & configDirName let platformName = when defined(windows): "windows" @@ -151,8 +154,6 @@ type closeUnusedDocumentsTask: DelayedTask - homeDir: string - var gEditor* {.exportc.}: App = nil proc handleLog(self: App, level: Level, args: openArray[string]) @@ -447,67 +448,23 @@ proc loadKeybindings*(self: App, directory: string, except CatchableError: log(lvlError, fmt"Failed to load keybindings from {filenames[i]}: {getCurrentExceptionMsg()}") -proc loadConfigFileFromAppDir(self: App, context: string, path: string): - Future[Option[string]] {.async.} = - try: - let content = await self.vfs.read("app://" & path) - log lvlInfo, &"Loaded {context} from app:{path}" - return content.some - except FileNotFoundError: - return string.none - except CatchableError: - log(lvlError, fmt"Failed to load {context} from app dir: {getCurrentExceptionMsg()}") - - return string.none - -proc loadConfigFileFromHomeDir(self: App, context: string, path: string): - Future[Option[string]] {.async.} = - try: - if self.homeDir.len == 0: - log lvlInfo, &"No home directory" - return - - let content = await self.vfs.read(self.homeDir // path) - log lvlInfo, &"Loaded {context} from {self.homeDir}/{path}" - return content.some - except FileNotFoundError: - return string.none - except CatchableError: - log(lvlError, fmt"Failed to load {context} from home {self.homeDir}: {getCurrentExceptionMsg()}") - - return string.none - -proc loadConfigFileFromWorkspaceDir(self: App, context: string, path: string): +proc loadConfigFileFrom(self: App, context: string, path: string): Future[Option[string]] {.async.} = try: - let absolutePath = self.workspace.getAbsolutePath(path) - let content = await self.vfs.read(absolutePath) - log lvlInfo, &"Loaded {context} from {self.workspace.name}/{path}" + let content = await self.vfs.read(path) + log lvlInfo, &"Loaded {context} from '{path}'" return content.some except FileNotFoundError: return string.none except CatchableError: - log(lvlError, - fmt"Failed to load {context} from workspace {self.workspace.name}: {getCurrentExceptionMsg()}") + log(lvlError, fmt"Failed to load {context} from '{path}': {getCurrentExceptionMsg()}") return string.none -proc loadOptionsFromAppDir*(self: App) {.async.} = - await allFutures( - self.loadSettingsFrom("config", loadConfigFileFromAppDir), - self.loadKeybindings("config", loadConfigFileFromAppDir) - ) - -proc loadOptionsFromHomeDir*(self: App) {.async.} = - await allFutures( - self.loadSettingsFrom(configDirName, loadConfigFileFromHomeDir), - self.loadKeybindings(configDirName, loadConfigFileFromHomeDir) - ) - -proc loadOptionsFromWorkspace*(self: App) {.async.} = +proc loadConfigFrom*(self: App, root: string) {.async.} = await allFutures( - self.loadSettingsFrom(configDirName, loadConfigFileFromWorkspaceDir), - self.loadKeybindings(configDirName, loadConfigFileFromWorkspaceDir) + self.loadSettingsFrom(root, loadConfigFileFrom), + self.loadKeybindings(root, loadConfigFileFrom) ) # import asynchttpserver, asyncnet @@ -668,10 +625,6 @@ proc newApp*(backend: api.Backend, platform: Platform, services: Services, optio self.timer = startTimer() self.frameTimer = startTimer() - self.homeDir = getHomeDir().normalizePathUnix.catch: - log lvlError, &"Failed to get home directory: {getCurrentExceptionMsg()}" - "" - self.layout = services.getService(LayoutService).get self.config = services.getService(ConfigService).get self.editors = services.getService(DocumentEditorService).get @@ -700,8 +653,8 @@ proc newApp*(backend: api.Backend, platform: Platform, services: Services, optio self.setupDefaultKeybindings() - await self.loadOptionsFromAppDir() - await self.loadOptionsFromHomeDir() + await self.loadConfigFrom(appConfigDir) + await self.loadConfigFrom(homeConfigDir) log lvlInfo, &"Finished loading app and user settings" self.applySettingsFromAppOptions() @@ -804,13 +757,13 @@ proc finishInitialization*(self: App, state: EditorState) {.async.} = for wf in state.workspaceFolders: log(lvlInfo, fmt"Restoring workspace") self.workspace.restore(wf.settings) - await self.loadOptionsFromWorkspace() + await self.loadConfigFrom(workspaceConfigDir) # Open current working dir as local workspace if no workspace exists yet if self.workspace.path == "": log lvlInfo, "No workspace open yet, opening current working directory as local workspace" self.workspace.addWorkspaceFolder(getCurrentDir().normalizePathUnix) - await self.loadOptionsFromWorkspace() + await self.loadConfigFrom(workspaceConfigDir) # Restore open editors if self.appOptions.fileToOpen.getSome(filePath): @@ -914,11 +867,11 @@ proc reapplyConfigKeybindingsAsync(self: App, app: bool = false, home: bool = fa {.async.} = log lvlInfo, &"reapplyConfigKeybindingsAsync app={app}, home={home}, workspace={workspace}" if app: - await self.loadKeybindings("config", loadConfigFileFromAppDir) + await self.loadKeybindings(appConfigDir, loadConfigFileFrom) if home: - await self.loadKeybindings(configDirName, loadConfigFileFromHomeDir) + await self.loadKeybindings(homeConfigDir, loadConfigFileFrom) if workspace: - await self.loadKeybindings(configDirName, loadConfigFileFromWorkspaceDir) + await self.loadKeybindings(workspaceConfigDir, loadConfigFileFrom) proc reapplyConfigKeybindings*(self: App, app: bool = false, home: bool = false, workspace: bool = false) {.expose("editor").} = @@ -1350,24 +1303,25 @@ proc chooseTheme*(self: App) {.expose("editor").} = let originalTheme = self.theme.path - proc getItems(): seq[FinderItem] {.gcsafe, raises: [].} = + proc getItems(): Future[ItemList] {.gcsafe, async: (raises: []).} = var items = newSeq[FinderItem]() let themesDir = "app://themes" try: - for file in walkDirRec(themesDir, relative=true): - if file.endsWith ".json": + let files = self.vfs.getDirectoryListingRec(Globs(), themesDir).await + for file in files: + if file.endsWith(".json"): let (relativeDirectory, name, _) = file.splitFile items.add FinderItem( displayName: name, - data: fmt"{themesDir}/{file}", - detail: "themes" / relativeDirectory, + data: file, + detail: relativeDirectory, ) except: discard - return items + return newItemList(items) - let source = newSyncDataSource(getItems) + let source = newAsyncCallbackDataSource(getItems) var finder = newFinder(source, filterAndSort=true) finder.filterThreshold = float.low @@ -1504,7 +1458,7 @@ proc browseKeybinds*(self: App, preview: bool = true, scaleX: float = 0.9, scale let pathNorm = self.vfs.normalize(path) - let editor = self.layout.openWorkspaceFile(pathNorm) + let editor = self.layout.openFile(pathNorm) if editor.getSome(editor) and editor of TextDocumentEditor and targetSelection.isSome: editor.TextDocumentEditor.targetSelection = targetSelection.get editor.TextDocumentEditor.centerCursor() @@ -1532,7 +1486,7 @@ proc chooseFile*(self: App, preview: bool = true, scaleX: float = 0.8, scaleY: f popup.previewScale = previewScale popup.handleItemConfirmed = proc(item: FinderItem): bool = - discard self.layout.openWorkspaceFile(item.data) + discard self.layout.openFile(item.data) return true self.layout.pushPopup popup @@ -1693,7 +1647,7 @@ proc gotoNextLocation*(self: App) {.expose("editor").} = log lvlInfo, &"[gotoNextLocation] Found {path}:{location}" - let editor = self.layout.openWorkspaceFile(path) + let editor = self.layout.openFile(path) if editor.getSome(editor) and editor of TextDocumentEditor and location.isSome: editor.TextDocumentEditor.targetSelection = location.get.toSelection editor.TextDocumentEditor.centerCursor() @@ -1712,7 +1666,7 @@ proc gotoPrevLocation*(self: App) {.expose("editor").} = log lvlInfo, &"[gotoPrevLocation] Found {path}:{location}" - let editor = self.layout.openWorkspaceFile(path) + let editor = self.layout.openFile(path) if editor.getSome(editor) and editor of TextDocumentEditor and location.isSome: editor.TextDocumentEditor.targetSelection = location.get.toSelection editor.TextDocumentEditor.centerCursor() @@ -1741,7 +1695,7 @@ proc chooseLocation*(self: App) {.expose("editor").} = if popup.getPreviewSelection().getSome(selection): targetSelection = selection.some - let editor = self.layout.openWorkspaceFile(path) + let editor = self.layout.openFile(path) if editor.getSome(editor) and editor of TextDocumentEditor and targetSelection.isSome: editor.TextDocumentEditor.targetSelection = targetSelection.get editor.TextDocumentEditor.centerCursor() @@ -1750,7 +1704,7 @@ proc chooseLocation*(self: App) {.expose("editor").} = self.layout.pushPopup popup -proc searchWorkspaceItemList(workspace: Workspace, query: string, maxResults: int): Future[ItemList] {.async.} = +proc searchWorkspaceItemList(workspace: Workspace, query: string, maxResults: int): Future[ItemList] {.async: (raises: []).} = let searchResults = workspace.searchWorkspace(query, maxResults).await log lvlInfo, fmt"Found {searchResults.len} results" @@ -1832,7 +1786,7 @@ proc searchGlobalInteractive*(self: App) {.expose("editor").} = if popup.getPreviewSelection().getSome(selection): targetSelection = selection.some - let editor = self.layout.openWorkspaceFile(path) + let editor = self.layout.openFile(path) if editor.getSome(editor) and editor of TextDocumentEditor and targetSelection.isSome: editor.TextDocumentEditor.targetSelection = targetSelection.get editor.TextDocumentEditor.centerCursor() @@ -1844,8 +1798,11 @@ proc searchGlobal*(self: App, query: string) {.expose("editor").} = defer: self.platform.requestRender() - let maxResults = self.config.getOption[:int]("editor.max-search-results", 1000) - let source = newAsyncCallbackDataSource () => self.workspace.searchWorkspaceItemList(query, maxResults) + proc getItems(): Future[ItemList] {.gcsafe, async: (raises: []).} = + let maxResults = self.config.getOption[:int]("editor.max-search-results", 1000) + return self.workspace.searchWorkspaceItemList(query, maxResults).await + + let source = newAsyncCallbackDataSource(getItems) var finder = newFinder(source, filterAndSort=true) var popup = newSelectorPopup(self.services, "search".some, finder.some, @@ -1863,7 +1820,7 @@ proc searchGlobal*(self: App, query: string) {.expose("editor").} = if popup.getPreviewSelection().getSome(selection): targetSelection = selection.some - let editor = self.layout.openWorkspaceFile(path) + let editor = self.layout.openFile(path) if editor.getSome(editor) and editor of TextDocumentEditor and targetSelection.isSome: editor.TextDocumentEditor.targetSelection = targetSelection.get editor.TextDocumentEditor.centerCursor() @@ -1969,7 +1926,7 @@ proc installTreesitterParser*(self: App, language: string, host: string = "githu asyncSpawn self.installTreesitterParserAsync(language, host) -proc getItemsFromDirectory(vfs: VFS, workspace: Workspace, directory: string): Future[ItemList] {.async.} = +proc getItemsFromDirectory(vfs: VFS, workspace: Workspace, directory: string, showVFS: bool = false): Future[ItemList] {.async: (raises: []).} = let listing = await vfs.getDirectoryListing(directory) @@ -1986,6 +1943,14 @@ proc getItemsFromDirectory(vfs: VFS, workspace: Workspace, directory: string): F if relativeDirectory == ".": relativeDirectory = "" + var detail = directory // name + if showVFS: + let (vfs, rel) = vfs.getVFS(detail) + detail.add "\t" + detail.add vfs.name + detail.add "/" + detail.add rel + let icon = if isFile: fileIcon else: folderIcon list[i] = FinderItem( displayName: icon & " " & name, @@ -1994,7 +1959,7 @@ proc getItemsFromDirectory(vfs: VFS, workspace: Workspace, directory: string): F "path": directory // name, "isFile": isFile, }, - detail: directory // name, + detail: detail, ) inc i @@ -2006,7 +1971,7 @@ proc getItemsFromDirectory(vfs: VFS, workspace: Workspace, directory: string): F return list -proc exploreFiles*(self: App, root: string = "") {.expose("editor").} = +proc exploreFiles*(self: App, root: string = "", showVFS: bool = false, normalize: bool = true) {.expose("editor").} = defer: self.platform.requestRender() @@ -2015,7 +1980,10 @@ proc exploreFiles*(self: App, root: string = "") {.expose("editor").} = let currentDirectory = new string currentDirectory[] = root - let source = newAsyncCallbackDataSource () => getItemsFromDirectory(self.vfs, self.workspace, currentDirectory[]) + proc getItems(): Future[ItemList] {.gcsafe, async: (raises: []).} = + return getItemsFromDirectory(self.vfs, self.workspace, currentDirectory[], showVFS).await + + let source = newAsyncCallbackDataSource(getItems) var finder = newFinder(source, filterAndSort=true) finder.filterThreshold = float.low @@ -2029,8 +1997,12 @@ proc exploreFiles*(self: App, root: string = "") {.expose("editor").} = log lvlError, fmt"Failed to parse file info from item: {item}" return true + var path = fileInfo.path + if normalize: + path = self.vfs.normalize(path) + if fileInfo.isFile: - if self.layout.openWorkspaceFile(fileInfo.path).getSome(editor): + if self.layout.openFile(path).getSome(editor): if editor of TextDocumentEditor and popup.getPreviewSelection().getSome(selection): editor.TextDocumentEditor.selection = selection editor.TextDocumentEditor.centerCursor() @@ -2041,6 +2013,26 @@ proc exploreFiles*(self: App, root: string = "") {.expose("editor").} = source.retrigger() return false + popup.addCustomCommand "enter-normalized", proc(popup: SelectorPopup, args: JsonNode): bool = + if popup.getSelectedItem().getSome(item): + let fileInfo = item.data.parseJson.jsonTo(tuple[path: string, isFile: bool]).catch: + log lvlError, fmt"Failed to parse file info from item: {item}" + return true + + let path = self.vfs.normalize(fileInfo.path) + + if fileInfo.isFile: + if self.layout.openFile(path).getSome(editor): + if editor of TextDocumentEditor and popup.getPreviewSelection().getSome(selection): + editor.TextDocumentEditor.selection = selection + editor.TextDocumentEditor.centerCursor() + return true + else: + currentDirectory[] = path + popup.textEditor.document.content = "" + source.retrigger() + return false + popup.addCustomCommand "go-up", proc(popup: SelectorPopup, args: JsonNode): bool = let parent = currentDirectory[].parentDirectory log lvlInfo, fmt"go up: {currentDirectory[]} -> {parent}" @@ -2052,19 +2044,6 @@ proc exploreFiles*(self: App, root: string = "") {.expose("editor").} = self.layout.pushPopup popup -proc exploreUserConfigDir*(self: App) {.expose("editor").} = - if self.homeDir.len == 0: - log lvlInfo, &"No home directory" - return - - self.exploreFiles(self.homeDir // configDirName) - -proc exploreAppConfigDir*(self: App) {.expose("editor").} = - self.exploreFiles("app://config") - -proc exploreHelp*(self: App) {.expose("editor").} = - self.exploreFiles("app://docs") - proc exploreWorkspacePrimary*(self: App) {.expose("editor").} = self.exploreFiles(self.workspace.getWorkspacePath()) @@ -2126,9 +2105,9 @@ proc reloadPluginAsync*(self: App) {.async.} = log lvlError, &"Failed to reload wasm plugins: {getCurrentExceptionMsg()}\n{getCurrentException().getStackTrace()}" proc reloadConfigAsync*(self: App) {.async.} = - await self.loadOptionsFromAppDir() - await self.loadOptionsFromHomeDir() - await self.loadOptionsFromWorkspace() + await self.loadConfigFrom(appConfigDir) + await self.loadConfigFrom(homeConfigDir) + await self.loadConfigFrom(workspaceConfigDir) proc reloadConfig*(self: App, clearOptions: bool = false) {.expose("editor").} = ## Reloads settings.json and keybindings.json from the app directory, home directory and workspace diff --git a/src/document.nim b/src/document.nim index 14f21134..2b23a7bc 100644 --- a/src/document.nim +++ b/src/document.nim @@ -1,5 +1,5 @@ -import std/[os] import misc/util +import vfs type Document* = ref object of RootObj appFile*: bool ## Whether this is an application file (e.g. stored in local storage on the browser) @@ -8,6 +8,7 @@ type Document* = ref object of RootObj revision*: int undoableRevision*: int lastSavedRevision*: int ## Undobale revision at the time we saved the last time + vfs*: VFS method `$`*(document: Document): string {.base, gcsafe, raises: [].} = return "" method save*(self: Document, filename: string = "", app: bool = false) {.base, gcsafe, raises: [].} = discard @@ -15,15 +16,7 @@ method load*(self: Document, filename: string = "") {.base, gcsafe, raises: [].} method deinit*(self: Document) {.base, gcsafe, raises: [].} = discard method getStatisticsString*(self: Document): string {.base, gcsafe, raises: [].} = discard -proc fullPath*(self: Document): string {.gcsafe, raises: [].} = - # todo: normalize path - if not self.isBackedByFile: - return self.filename - - if self.filename.isAbsolute: - return self.filename - - try: - return self.filename.absolutePath - except ValueError, OSError: - return self.filename +proc normalizedPath*(self: Document): string {.gcsafe, raises: [].} = + if self.vfs.isNotNil: + return self.vfs.normalize(self.filename) + return self.filename diff --git a/src/finder/finder.nim b/src/finder/finder.nim index a83982bb..8289c72d 100644 --- a/src/finder/finder.nim +++ b/src/finder/finder.nim @@ -320,7 +320,7 @@ type AsyncCallbackDataSource* = ref object of DataSource wasQueried: bool = false - callback: proc(): Future[ItemList] {.gcsafe, raises: [].} + callback: proc(): Future[ItemList] {.gcsafe, async: (raises: []).} SyncDataSource* = ref object of DataSource wasQueried: bool = false @@ -339,7 +339,7 @@ proc newAsyncFutureDataSource*(future: Future[ItemList]): AsyncFutureDataSource new result result.future = future -proc newAsyncCallbackDataSource*(callback: proc(): Future[ItemList] {.gcsafe, raises: [].}): AsyncCallbackDataSource = +proc newAsyncCallbackDataSource*(callback: proc(): Future[ItemList] {.gcsafe, async: (raises: []).}): AsyncCallbackDataSource = new result result.callback = callback diff --git a/src/misc/util.nim b/src/misc/util.nim index 67b882dd..09f464e9 100644 --- a/src/misc/util.nim +++ b/src/misc/util.nim @@ -236,3 +236,22 @@ func find*[T](arr: openArray[T], val: T, start: int = 0): int = for i in start..= prefixLen: return true + if i >= sLen or s[i] != prefix[i]: return false + inc(i) + +func endsWith*[T](s, suffix: openArray[T]): bool = + let suffixLen = suffix.len + let sLen = s.len + var i = 0 + var j = sLen - suffixLen + while i+j >= 0 and i+j < sLen: + if s[i+j] != suffix[i]: return false + inc(i) + if i >= suffixLen: return true diff --git a/src/scripting/expose.nim b/src/scripting/expose.nim index 0d0ff88a..425a2a33 100644 --- a/src/scripting/expose.nim +++ b/src/scripting/expose.nim @@ -77,6 +77,7 @@ proc removePragmas(node: var NimNode) = param[0] = name macro expose*(moduleName: static string, def: untyped): untyped = + # echo moduleName, "::", def.name.repr if not exposeScriptingApi: return def diff --git a/src/scripting/scripting_wasm.nim b/src/scripting/scripting_wasm.nim index 8b02cd60..88d7ec5e 100644 --- a/src/scripting/scripting_wasm.nim +++ b/src/scripting/scripting_wasm.nim @@ -54,7 +54,7 @@ proc loadModules(self: ScriptContextWasm, path: string): Future[void] {.async.} let module = await newWasmModule(file, @[editorImports], self.vfs) if module.getSome(module): - self.moduleVfs.mount(file.splitFile.name & "/", newInMemoryVFS()) + self.moduleVfs.mount(file.splitFile.name, newInMemoryVFS()) self.stack.add module defer: discard self.stack.pop diff --git a/src/text/completion_provider_lsp.nim b/src/text/completion_provider_lsp.nim index 8873ee41..3fec4877 100644 --- a/src/text/completion_provider_lsp.nim +++ b/src/text/completion_provider_lsp.nim @@ -54,7 +54,7 @@ proc getLspCompletionsAsync(self: CompletionProviderLsp) {.async.} = await sleepAsync(2.milliseconds) # debugf"[getLspCompletionsAsync] start" - let completions = await self.languageServer.getCompletions(self.document.fullPath, location) + let completions = await self.languageServer.getCompletions(self.document.normalizedPath, location) if completions.isSuccess: log lvlInfo, fmt"[getLspCompletionsAsync] at {location}: got {completions.result.items.len} completions" self.unfilteredCompletions = completions.result.items diff --git a/src/text/language/language_server_lsp.nim b/src/text/language/language_server_lsp.nim index 67868545..0e159fa1 100644 --- a/src/text/language/language_server_lsp.nim +++ b/src/text/language/language_server_lsp.nim @@ -572,19 +572,19 @@ method connect*(self: LanguageServerLSP, document: Document) = var handle = new Id handle[] = document.onLoaded.subscribe proc(document: TextDocument): void = document.onLoaded.unsubscribe handle[] - asyncSpawn self.client.notifyTextDocumentOpenedChannel.send (self.languageId, document.fullPath, document.contentString) + asyncSpawn self.client.notifyTextDocumentOpenedChannel.send (self.languageId, document.normalizedPath, document.contentString) else: - asyncSpawn self.client.notifyTextDocumentOpenedChannel.send (self.languageId, document.fullPath, document.contentString) + asyncSpawn self.client.notifyTextDocumentOpenedChannel.send (self.languageId, document.normalizedPath, document.contentString) let onEditHandle = document.onEdit.subscribe proc(args: auto): void {.gcsafe, raises: [].} = - # debugf"TEXT INSERTED {args.document.fullPath}:{args.location}: {args.text}" + # debugf"TEXT INSERTED {args.document.normalizedPath}:{args.location}: {args.text}" # todo: we should batch these, as onEdit can be called multiple times per frame # especially for full document sync let version = args.document.buffer.history.versions.high - let fullPath = args.document.fullPath + let normalizedPath = args.document.normalizedPath if self.fullDocumentSync: - asyncSpawn self.client.notifyTextDocumentChangedChannel.send (fullPath, version, @[], args.document.contentString) + asyncSpawn self.client.notifyTextDocumentChangedChannel.send (normalizedPath, version, @[], args.document.contentString) else: var c = args.document.buffer.visibleText.cursorT(Point) # todo: currently relies on edits being sorted @@ -593,7 +593,7 @@ method connect*(self: LanguageServerLSP, document: Document) = let text = c.slice(Point.init(it.new.last.line, it.new.last.column)) TextDocumentContentChangeEvent(range: language_server_base.toLspRange(it.old), text: $text) ) - asyncSpawn self.client.notifyTextDocumentChangedChannel.send (fullPath, version, changes, "") + asyncSpawn self.client.notifyTextDocumentChangedChannel.send (normalizedPath, version, changes, "") self.documentHandles.add (document.Document, onEditHandle) @@ -613,8 +613,8 @@ method disconnect*(self: LanguageServerLSP, document: Document) {.gcsafe, raises self.documentHandles.removeSwap i break - # asyncSpawn self.client.notifyClosedTextDocument(document.fullPath) - asyncSpawn self.client.notifyTextDocumentClosedChannel.send(document.fullPath) + # asyncSpawn self.client.notifyClosedTextDocument(document.normalizedPath) + asyncSpawn self.client.notifyTextDocumentClosedChannel.send(document.normalizedPath) if self.documentHandles.len == 0: self.stop() diff --git a/src/text/text_document.nim b/src/text/text_document.nim index 9cb4c49d..a8861d4a 100644 --- a/src/text/text_document.nim +++ b/src/text/text_document.nim @@ -60,7 +60,6 @@ type buffer*: Buffer mLanguageId: string services: Services - vfs: VFS workspace: Workspace nextLineIdCounter: int32 = 0 diff --git a/src/text/text_editor.nim b/src/text/text_editor.nim index f8f8c3bc..0db0c184 100644 --- a/src/text/text_editor.nim +++ b/src/text/text_editor.nim @@ -2519,7 +2519,7 @@ proc gotoDefinitionAsync(self: TextDocumentEditor): Future[void] {.async.} = if languageServer.getSome(ls): # todo: absolute paths - let locations = await ls.getDefinition(self.document.fullPath, self.selection.last) + let locations = await ls.getDefinition(self.document.normalizedPath, self.selection.last) await self.gotoLocationAsync(locations) proc gotoDeclarationAsync(self: TextDocumentEditor): Future[void] {.async.} = @@ -2528,7 +2528,7 @@ proc gotoDeclarationAsync(self: TextDocumentEditor): Future[void] {.async.} = return if languageServer.getSome(ls): - let locations = await ls.getDeclaration(self.document.fullPath, self.selection.last) + let locations = await ls.getDeclaration(self.document.normalizedPath, self.selection.last) await self.gotoLocationAsync(locations) proc gotoTypeDefinitionAsync(self: TextDocumentEditor): Future[void] {.async.} = @@ -2537,7 +2537,7 @@ proc gotoTypeDefinitionAsync(self: TextDocumentEditor): Future[void] {.async.} = return if languageServer.getSome(ls): - let locations = await ls.getTypeDefinition(self.document.fullPath, self.selection.last) + let locations = await ls.getTypeDefinition(self.document.normalizedPath, self.selection.last) await self.gotoLocationAsync(locations) proc gotoImplementationAsync(self: TextDocumentEditor): Future[void] {.async.} = @@ -2546,7 +2546,7 @@ proc gotoImplementationAsync(self: TextDocumentEditor): Future[void] {.async.} = return if languageServer.getSome(ls): - let locations = await ls.getImplementation(self.document.fullPath, self.selection.last) + let locations = await ls.getImplementation(self.document.normalizedPath, self.selection.last) await self.gotoLocationAsync(locations) proc gotoReferencesAsync(self: TextDocumentEditor): Future[void] {.async.} = @@ -2555,7 +2555,7 @@ proc gotoReferencesAsync(self: TextDocumentEditor): Future[void] {.async.} = return if languageServer.getSome(ls): - let locations = await ls.getReferences(self.document.fullPath, self.selection.last) + let locations = await ls.getReferences(self.document.normalizedPath, self.selection.last) await self.gotoLocationAsync(locations) proc switchSourceHeaderAsync(self: TextDocumentEditor): Future[void] {.async.} = @@ -2564,7 +2564,7 @@ proc switchSourceHeaderAsync(self: TextDocumentEditor): Future[void] {.async.} = return if languageServer.getSome(ls): - let filename = await ls.switchSourceHeader(self.document.fullPath) + let filename = await ls.switchSourceHeader(self.document.normalizedPath) if filename.getSome(filename): discard self.layout.openFile(filename) @@ -2689,7 +2689,7 @@ proc gotoSymbolAsync(self: TextDocumentEditor): Future[void] {.async.} = if self.document.getLanguageServer().await.getSome(ls): if self.document.isNil: return - let symbols = await ls.getSymbols(self.document.fullPath) + let symbols = await ls.getSymbols(self.document.normalizedPath) if symbols.len == 0: return @@ -3000,7 +3000,7 @@ proc showHoverForAsync(self: TextDocumentEditor, cursor: Cursor): Future[void] { return if languageServer.getSome(ls): - let hoverInfo = await ls.getHover(self.document.fullPath, cursor) + let hoverInfo = await ls.getHover(self.document.normalizedPath, cursor) if hoverInfo.getSome(hoverInfo): self.showHover = true self.hoverScrollOffset = 0 @@ -3070,7 +3070,7 @@ proc updateInlayHintsAsync*(self: TextDocumentEditor): Future[void] {.async.} = let visibleRange = self.visibleTextRange(self.screenLineCount) let snapshot = self.document.buffer.snapshot.clone() - let inlayHints: Response[seq[language_server_base.InlayHint]] = await ls.getInlayHints(self.document.fullPath, visibleRange) + let inlayHints: Response[seq[language_server_base.InlayHint]] = await ls.getInlayHints(self.document.normalizedPath, visibleRange) # todo: detect if canceled instead if inlayHints.isSuccess: # log lvlInfo, fmt"Updating inlay hints: {inlayHints}" diff --git a/src/vcs/vcs_api.nim b/src/vcs/vcs_api.nim index da46c38a..0f86c18c 100644 --- a/src/vcs/vcs_api.nim +++ b/src/vcs/vcs_api.nim @@ -19,46 +19,50 @@ proc getVCSService(): Option[VCSService] = static: addInjector(VCSService, getVCSService) -proc getChangedFilesFromGitAsync(self: VCSService, workspace: Workspace, all: bool): Future[ItemList] {.async.} = +proc getChangedFilesFromGitAsync(self: VCSService, workspace: Workspace, all: bool): Future[ItemList] {.async: (raises: []).} = let vcsList = self.getAllVersionControlSystems() var items = newSeq[FinderItem]() for vcs in vcsList: - let fileInfos = await vcs.getChangedFiles() - - for info in fileInfos: - let (directory, name) = info.path.splitPath - var relativeDirectory = workspace.getRelativePathSync(directory).get(directory) - - if relativeDirectory == ".": - relativeDirectory = "" - - if info.stagedStatus != None and info.stagedStatus != Untracked: - var info1 = info - info1.unstagedStatus = None - - var info2 = info - info2.stagedStatus = None - - items.add FinderItem( - displayName: $info1.stagedStatus & $info1.unstagedStatus & " " & name, - data: $ %info1, - detail: relativeDirectory & "\t" & vcs.root, - ) - items.add FinderItem( - displayName: $info2.stagedStatus & $info2.unstagedStatus & " " & name, - data: $ %info2, - detail: relativeDirectory & "\t" & vcs.root, - ) - else: - items.add FinderItem( - displayName: $info.stagedStatus & $info.unstagedStatus & " " & name, - data: $ %info, - detail: relativeDirectory & "\t" & vcs.root, - ) - - if not all: - break + try: + let fileInfos = await vcs.getChangedFiles() + + for info in fileInfos: + let (directory, name) = info.path.splitPath + var relativeDirectory = workspace.getRelativePathSync(directory).get(directory) + + if relativeDirectory == ".": + relativeDirectory = "" + + if info.stagedStatus != None and info.stagedStatus != Untracked: + var info1 = info + info1.unstagedStatus = None + + var info2 = info + info2.stagedStatus = None + + items.add FinderItem( + displayName: $info1.stagedStatus & $info1.unstagedStatus & " " & name, + data: $ %info1, + detail: relativeDirectory & "\t" & vcs.root, + ) + items.add FinderItem( + displayName: $info2.stagedStatus & $info2.unstagedStatus & " " & name, + data: $ %info2, + detail: relativeDirectory & "\t" & vcs.root, + ) + else: + items.add FinderItem( + displayName: $info.stagedStatus & $info.unstagedStatus & " " & name, + data: $ %info, + detail: relativeDirectory & "\t" & vcs.root, + ) + + if not all: + break + + except CatchableError: + log lvlError, &"Failed to get changed files from {vcs.root}" return newItemList(items) diff --git a/src/vfs.nim b/src/vfs.nim index 1f73814c..8897c186 100644 --- a/src/vfs.nim +++ b/src/vfs.nim @@ -38,7 +38,7 @@ type target*: VFS targetPrefix*: string -proc getVFS*(self: VFS, path: string, maxDepth: int = int.high): tuple[vfs: VFS, relativePath: string] +proc getVFS*(self: VFS, path: openArray[char], maxDepth: int = int.high): tuple[vfs: VFS, relativePath: string] proc read*(self: VFS, path: string, flags: set[ReadFlag] = {}): Future[string] {.async: (raises: [IOError]).} proc write*(self: VFS, path: string, content: string): Future[void] {.async: (raises: [IOError]).} proc write*(self: VFS, path: string, content: sink RopeSlice[int]): Future[void] {.async: (raises: [IOError]).} @@ -49,21 +49,33 @@ proc getDirectoryListing*(self: VFS, path: string): Future[DirectoryListing] {.a proc normalizePathUnix*(path: string): string = var stripLeading = false + var stripTrailing = true if path.startsWith("/") and path.len >= 3 and path[2] == ':': # Windows path: /C:/... stripLeading = true - result = path.normalizedPath.replace('\\', '/').strip(leading=stripLeading, chars={'/'}) + + if path.endsWith("://"): + stripTrailing = false + + result = path.normalizedPath.replace('\\', '/').strip(leading=stripLeading, trailing=stripTrailing, chars={'/'}) if result.len >= 2 and result[1] == ':': result[0] = result[0].toUpperAscii proc normalizeNativePath*(path: string): string = var stripLeading = false + var stripTrailing = true when defined(windows): if path.startsWith("/") and path.len >= 3 and path[2] == ':': # Windows path: /C:/... stripLeading = true result = path.replace('\\', '/') - result = result.strip(leading=stripLeading, chars={'/'}) + else: + result = path + + if path.endsWith("://"): + stripTrailing = false + + result = result.strip(leading=stripLeading, trailing=stripTrailing, chars={'/'}) when defined(windows): if result.len >= 2 and result[1] == ':': result[0] = result[0].toUpperAscii @@ -124,7 +136,7 @@ method getFileKindImpl*(self: VFS, path: string): Future[Option[FileKind]] {.bas method getFileAttributesImpl*(self: VFS, path: string): Future[Option[FileAttributes]] {.base, async: (raises: []).} = return FileAttributes.none -method getVFSImpl*(self: VFS, path: string, maxDepth: int = int.high): tuple[vfs: VFS, relativePath: string] {.base.} = +method getVFSImpl*(self: VFS, path: openArray[char], maxDepth: int = int.high): tuple[vfs: VFS, relativePath: string] {.base.} = (nil, "") method normalizeImpl*(self: VFS, path: string): string {.base.} = @@ -164,13 +176,13 @@ method getDirectoryListingImpl*(self: VFSNull, path: string): Future[DirectoryLi method name*(self: VFSLink): string = &"VFSLink({self.prefix}, {self.target.name}/{self.targetPrefix})" method readImpl*(self: VFSLink, path: string, flags: set[ReadFlag]): Future[string] {.async: (raises: [IOError]).} = - return self.target.read(self.targetPrefix & path, flags).await + return self.target.read(self.targetPrefix // path, flags).await method writeImpl*(self: VFSLink, path: string, content: string): Future[void] {.async: (raises: [IOError]).} = - await self.target.write(self.targetPrefix & path, content) + await self.target.write(self.targetPrefix // path, content) method writeImpl*(self: VFSLink, path: string, content: sink RopeSlice[int]): Future[void] {.async: (raises: [IOError]).} = - await self.target.write(self.targetPrefix & path, content.move) + await self.target.write(self.targetPrefix // path, content.move) method getFileKindImpl*(self: VFSLink, path: string): Future[Option[FileKind]] {.async: (raises: []).} = return self.target.getFileKind(path).await @@ -179,19 +191,19 @@ method getFileAttributesImpl*(self: VFSLink, path: string): Future[Option[FileAt return self.target.getFileAttributes(path).await method normalizeImpl*(self: VFSLink, path: string): string = - return self.target.normalize(self.targetPrefix & path) + return self.target.normalize(self.targetPrefix // path) method getDirectoryListingImpl*(self: VFSLink, path: string): Future[DirectoryListing] {.async: (raises: []).} = - return self.target.getDirectoryListing(self.targetPrefix & path).await + return self.target.getDirectoryListing(self.targetPrefix // path).await -method getVFSImpl*(self: VFSLink, path: string, maxDepth: int = int.high): tuple[vfs: VFS, relativePath: string] = +method getVFSImpl*(self: VFSLink, path: openArray[char], maxDepth: int = int.high): tuple[vfs: VFS, relativePath: string] = when debugLogVfs: - debugf"[{self.name}] '{self.prefix}' getVFSImpl({path}) -> ({self.target.name}, {self.target.prefix}), '{self.targetPrefix & path}'" + debugf"[{self.name}] '{self.prefix}' getVFSImpl({path.join()}) -> ({self.target.name}, {self.target.prefix}), '{self.targetPrefix // path.join()}'" if maxDepth == 0: - return (self, path) + return (self, path.join()) - return self.target.getVFS(self.targetPrefix & path) + return self.target.getVFS(self.targetPrefix // path.join(), maxDepth - 1) method name*(self: VFSInMemory): string = &"VFSInMemory({self.prefix})" @@ -227,25 +239,32 @@ method getDirectoryListingImpl*(self: VFSInMemory, path: string): Future[Directo if file.startsWith(path): result.files.add file -proc getVFS*(self: VFS, path: string, maxDepth: int = int.high): tuple[vfs: VFS, relativePath: string] = - # echo &"getVFS {self.name} '{path}'" - # defer: - # echo &" -> getVFS {self.name} '{path}' -> {result.vfs.name} '{result.relativePath}'" +proc getVFS*(self: VFS, path: openArray[char], maxDepth: int = int.high): tuple[vfs: VFS, relativePath: string] = + # when debugLogVfs: + # echo &"getVFS {self.name} '{path.join()}'" + # defer: + # echo &" -> getVFS {self.name} '{path.join()}' -> {result.vfs.name} '{result.relativePath}'" if maxDepth == 0: - return (self, path) + return (self, path.join()) for i in countdown(self.mounts.high, 0): - if path.startsWith(self.mounts[i].prefix): + template pref(): untyped = self.mounts[i].prefix + + if path.startsWith(pref.toOpenArray()) and (pref.len == 0 or path.len == pref.len or pref.endsWith(['/']) or path[pref.len] == '/'): + var prefixLen = pref.len + if pref.len > 0 and not pref.endsWith(['/']) and path.len > pref.len: + inc prefixLen + when debugLogVfs: - debugf"[{self.name}] '{self.prefix}' foward({path}) to ({self.mounts[i].vfs.name}, {self.mounts[i].vfs.prefix})" - return self.mounts[i].vfs.getVFS(path[self.mounts[i].prefix.len..^1], maxDepth - 1) + debugf"[{self.name}] '{self.prefix}' foward({path.join()}) to ({self.mounts[i].vfs.name}, {self.mounts[i].vfs.prefix})" + return self.mounts[i].vfs.getVFS(path[prefixLen..^1], maxDepth - 1) when debugLogVfs: - debugf"[{self.name}] '{self.prefix}' getVFSImpl({path})" + debugf"[{self.name}] '{self.prefix}' getVFSImpl({path.join()})" result = self.getVFSImpl(path, maxDepth) # todo: maxDepth - 1? if result.vfs.isNil: - result = (self, path) + result = (self, path.join("")) proc read*(self: VFS, path: string, flags: set[ReadFlag] = {}): Future[string] {.async: (raises: [IOError]).} = when debugLogVfs: @@ -294,10 +313,10 @@ proc normalize*(self: VFS, path: string): string = var (vfs, path) = self.getVFS(path) while vfs.parent.getSome(parent): - path = vfs.prefix & path + path = vfs.prefix // path vfs = parent - return path + return path.normalizeNativePath proc getDirectoryListing*(self: VFS, path: string): Future[DirectoryListing] {.async: (raises: []).} = # defer: @@ -314,6 +333,7 @@ proc getDirectoryListing*(self: VFS, path: string): Future[DirectoryListing] {.a proc mount*(self: VFS, prefix: string, vfs: VFS) = assert vfs.parent.isNone + log lvlInfo, &"{self.name}: mount {vfs.name} under '{prefix}'" for i in 0..self.mounts.high: if self.mounts[i].prefix == prefix: self.mounts[i].vfs.parent = VFS.none diff --git a/src/vfs_local.nim b/src/vfs_local.nim index ffe7201a..d2c28822 100644 --- a/src/vfs_local.nim +++ b/src/vfs_local.nim @@ -98,9 +98,9 @@ method normalizeImpl*(self: VFSLocal, path: string): string = except: return path -proc fillDirectoryListing(directoryListing: var DirectoryListing, path: string) = +proc fillDirectoryListing(directoryListing: var DirectoryListing, path: string, relative: bool = true) = try: - for (kind, name) in walkDir(path, relative=true): + for (kind, name) in walkDir(path, relative=relative): case kind of pcFile: directoryListing.files.add name @@ -136,9 +136,14 @@ method getDirectoryListingImpl*(self: VFSLocal, path: string): Future[DirectoryL index = nextIndex + 1 else: - result.fillDirectoryListing("/") + result.fillDirectoryListing("/", relative = false) else: + when defined(posix): + if path == "/": + result.fillDirectoryListing("/", relative = false) + return + result.fillDirectoryListing(path) diff --git a/src/vfs_service.nim b/src/vfs_service.nim index 474504a6..f0c62ae0 100644 --- a/src/vfs_service.nim +++ b/src/vfs_service.nim @@ -20,10 +20,18 @@ addBuiltinService(VFSService) method init*(self: VFSService): Future[Result[void, ref CatchableError]] {.async: (raises: []).} = log lvlInfo, &"VFSService.init" + self.vfs = VFS() self.vfs.mount("", VFSLocal()) self.vfs.mount("app://", VFSLink(target: self.vfs.getVFS("").vfs, targetPrefix: getAppDir().normalizeNativePath & "/")) self.vfs.mount("plugs://", VFSNull()) + self.vfs.mount("ws://", VFS()) + + let homeDir = getHomeDir().normalizePathUnix.catch: + log lvlError, &"Failed to get home directory: {getCurrentExceptionMsg()}" + "" + if homeDir != "": + self.vfs.mount("home://", VFSLink(target: self.vfs.getVFS("").vfs, targetPrefix: homeDir & "/")) return ok() @@ -81,6 +89,9 @@ proc mountVfs*(self: VFSService, parentPath: string, prefix: string, config: Jso if self.createVfs(config).getSome(newVFS): vfs.mount(prefix, newVFS) +proc normalizePath*(self: VFSService, path: string): string {.expose("vfs").} = + return self.vfs.normalize(path) + proc dumpVfsHierarchy*(self: VFSService) {.expose("vfs").} = log lvlInfo, "\n" & self.vfs.prettyHierarchy() diff --git a/src/workspaces/workspace.nim b/src/workspaces/workspace.nim index b96dd90a..cc40e2a8 100644 --- a/src/workspaces/workspace.nim +++ b/src/workspaces/workspace.nim @@ -131,45 +131,48 @@ proc getWorkspacePath*(self: Workspace): string = return "" proc searchWorkspaceFolder(self: Workspace, query: string, root: string, maxResults: int): - Future[seq[SearchResult]] {.async.} = - let output = runProcessAsync("rg", @["--line-number", "--column", "--heading", query, root], - maxLines=maxResults).await - var res: seq[SearchResult] - - var currentFile = "" - for line in output: - if currentFile == "": - if line.isAbsolute: - currentFile = line.normalizePathUnix - else: - currentFile = root // line - continue - - if line == "": - currentFile = "" - continue - - var separatorIndex1 = line.find(':') - if separatorIndex1 == -1: - continue - - let lineNumber = line[0..