diff --git a/CHANGES_CURRENT.md b/CHANGES_CURRENT.md index 7459d42d3b..e819e6d98e 100644 --- a/CHANGES_CURRENT.md +++ b/CHANGES_CURRENT.md @@ -9,6 +9,7 @@ - #3077 - Terminal: Use editor font as default (related #3062) - #3146 - Vim: Fix command-line staying open when clicking the editor or file explorer (fixes #3031) - #3161 - Configuration: Turn soft word-wrap on by default (fixes #3161) +- #3162 - Windows: Support opening UNC paths (fixes #3151) ### Performance diff --git a/integration_test/lib/Oni_IntegrationTestLib.re b/integration_test/lib/Oni_IntegrationTestLib.re index a320f543e8..bbdf1625b3 100644 --- a/integration_test/lib/Oni_IntegrationTestLib.re +++ b/integration_test/lib/Oni_IntegrationTestLib.re @@ -1,4 +1,5 @@ module Core = Oni_Core; +module FpExp = Core.FpExp; module Utility = Core.Utility; module Model = Oni_Model; @@ -157,7 +158,7 @@ let runTest = |> Printf.fprintf(oc, "%s\n"); close_out(oc); - tempFilePath |> Fp.absoluteCurrentPlatform |> Option.get; + tempFilePath |> FpExp.absoluteCurrentPlatform |> Option.get; }; let keybindingsFilePath = diff --git a/manual_test/cases.md b/manual_test/cases.md index e3f06f0baa..0b60f76170 100644 --- a/manual_test/cases.md +++ b/manual_test/cases.md @@ -215,6 +215,21 @@ __Pass:__ - [ ] OSX - [ ] Linux +## 8.2 Windows-style path handling + +### 8.2.1 Verify can `:cd` into a UNC path + +Regression test for #3151 + +- Open Onivim 2 +- `:cd` into a UNC path - for example: `\\\\LOCALHOST\\c$\\oni2` +- Verify the explorer is refreshed +- Verify directory nodes can be expanded +- Verify files can be opened + +__Pass:__ +- [ ] Win + # 9. Terminal ## 9.1 Check that `ONIVIM_TERMINAL` is set diff --git a/src/CLI/Oni_CLI.re b/src/CLI/Oni_CLI.re index 5e76f65bb0..caa4dd1e09 100644 --- a/src/CLI/Oni_CLI.re +++ b/src/CLI/Oni_CLI.re @@ -3,6 +3,7 @@ * * Module for handling command-line arguments for Oni2 */ +open Oni_Core; open Kernel; open Rench; @@ -14,7 +15,7 @@ type t = { folder: option(string), filesToOpen: list(string), forceScaleFactor: option(float), - overriddenExtensionsDir: option(Fp.t(Fp.absolute)), + overriddenExtensionsDir: option(FpExp.t(FpExp.absolute)), shouldLoadExtensions: bool, shouldLoadConfiguration: bool, shouldSyntaxHighlight: bool, @@ -271,7 +272,8 @@ let parse = (~getenv: string => option(string), args) => { forceScaleFactor: scaleFactor^, gpuAcceleration: gpuAcceleration^, overriddenExtensionsDir: - extensionsDir^ |> Utility.OptionEx.flatMap(Fp.absoluteCurrentPlatform), + extensionsDir^ + |> Utility.OptionEx.flatMap(FpExp.absoluteCurrentPlatform), shouldLoadExtensions: shouldLoadExtensions^, shouldLoadConfiguration: shouldLoadConfiguration^, shouldSyntaxHighlight: shouldSyntaxHighlight^, diff --git a/src/CLI/Oni_CLI.rei b/src/CLI/Oni_CLI.rei index 0d90814637..3ab8517999 100644 --- a/src/CLI/Oni_CLI.rei +++ b/src/CLI/Oni_CLI.rei @@ -1,9 +1,11 @@ +open Oni_Core; + type t = { gpuAcceleration: [ | `Auto | `ForceSoftware | `ForceHardware], folder: option(string), filesToOpen: list(string), forceScaleFactor: option(float), - overriddenExtensionsDir: option(Fp.t(Fp.absolute)), + overriddenExtensionsDir: option(FpExp.t(FpExp.absolute)), shouldLoadExtensions: bool, shouldLoadConfiguration: bool, shouldSyntaxHighlight: bool, diff --git a/src/Components/FileExplorer/Component_FileExplorer.re b/src/Components/FileExplorer/Component_FileExplorer.re index 1cc9d80317..9a2f337777 100644 --- a/src/Components/FileExplorer/Component_FileExplorer.re +++ b/src/Components/FileExplorer/Component_FileExplorer.re @@ -21,7 +21,7 @@ module Internal = { }; let luvDirentToFsTree = (~cwd, {name, kind}: Luv.File.Dirent.t) => { - let path = Fp.At.(cwd / name); + let path = FpExp.At.(cwd / name); if (kind == `FILE || kind == `LINK) { Some(FsTreeNode.file(path)); @@ -32,7 +32,7 @@ module Internal = { }; }; - let luvDirentsToFsTree = (~cwd: Fp.t(Fp.absolute), ~ignored, dirents) => { + let luvDirentsToFsTree = (~cwd: FpExp.t(FpExp.absolute), ~ignored, dirents) => { dirents |> List.filter(({name, _}: Luv.File.Dirent.t) => name != ".." && name != "." && !List.mem(name, ignored) @@ -53,12 +53,12 @@ module Internal = { */ let getFilesAndFolders = (~ignored, cwd) => { cwd - |> Fp.toString + |> FpExp.toString |> Service_OS.Api.readdir |> Lwt.map(luvDirentsToFsTree(~ignored, ~cwd)); }; - let getDirectoryTree = (cwd: Fp.t(Fp.absolute), ignored) => { + let getDirectoryTree = (cwd: FpExp.t(FpExp.absolute), ignored) => { let childrenPromise = getFilesAndFolders(~ignored, cwd); childrenPromise @@ -71,7 +71,7 @@ module Internal = { module Effects = { let load = (directory, configuration, ~onComplete) => { Isolinear.Effect.createWithDispatch(~name="explorer.load", dispatch => { - let directoryStr = Fp.toString(directory); + let directoryStr = FpExp.toString(directory); Log.infof(m => m("Loading nodes for directory: %s", directoryStr)); let ignored = Configuration.getValue(c => c.filesExclude, configuration); @@ -110,7 +110,7 @@ type outmsg = | GrabFocus; let setTree = (tree, model) => { - let uniqueId = (data: FsTreeNode.metadata) => Fp.toString(data.path); + let uniqueId = (data: FsTreeNode.metadata) => FpExp.toString(data.path); let (rootName, firstLevelChildren) = switch (tree) { | Tree.Leaf(_) => ("", []) @@ -282,14 +282,14 @@ let update = (~configuration, msg, model) => { c => c.workbenchEditorEnablePreview, configuration, ) - ? PreviewFile(Fp.toString(node.path)) - : OpenFile(Fp.toString(node.path)), + ? PreviewFile(FpExp.toString(node.path)) + : OpenFile(FpExp.toString(node.path)), ) | Component_VimTree.Selected(node) => // Set active here to avoid scrolling in BufferEnter ( model |> setActive(Some(node.path)), - OpenFile(Fp.toString(node.path)), + OpenFile(FpExp.toString(node.path)), ) | Component_VimTree.Nothing => (model, Nothing) }; @@ -313,7 +313,7 @@ let sub = (~configuration, {rootPath, _}) => { Service_OS.Sub.dir( ~uniqueId="FileExplorerSideBar", ~toMsg, - Fp.toString(rootPath), + FpExp.toString(rootPath), ); }; diff --git a/src/Components/FileExplorer/Component_FileExplorer.rei b/src/Components/FileExplorer/Component_FileExplorer.rei index ef0f0a87d0..40a7223c90 100644 --- a/src/Components/FileExplorer/Component_FileExplorer.rei +++ b/src/Components/FileExplorer/Component_FileExplorer.rei @@ -7,14 +7,14 @@ type msg; module Msg: { let keyPressed: string => msg; - let activeFileChanged: option(Fp.t(Fp.absolute)) => msg; + let activeFileChanged: option(FpExp.t(FpExp.absolute)) => msg; }; type model; -let initial: (~rootPath: Fp.t(Fp.absolute)) => model; -let setRoot: (~rootPath: Fp.t(Fp.absolute), model) => model; -let root: model => Fp.t(Fp.absolute); +let initial: (~rootPath: FpExp.t(FpExp.absolute)) => model; +let setRoot: (~rootPath: FpExp.t(FpExp.absolute), model) => model; +let root: model => FpExp.t(FpExp.absolute); let keyPress: (string, model) => model; diff --git a/src/Components/FileExplorer/FileTreeView.re b/src/Components/FileExplorer/FileTreeView.re index 49f74fe551..011f842499 100644 --- a/src/Components/FileExplorer/FileTreeView.re +++ b/src/Components/FileExplorer/FileTreeView.re @@ -73,8 +73,8 @@ let nodeView = let path = node.path; switch (decoration) { | Some((decoration: Feature_Decorations.Decoration.t)) => - Fp.toString(path) ++ " • " ++ decoration.tooltip - | None => Fp.toString(path) + FpExp.toString(path) ++ " • " ++ decoration.tooltip + | None => FpExp.toString(path) }; }; @@ -100,7 +100,7 @@ let make = ~focusedIndex, ~treeView: Component_VimTree.model(FsTreeNode.metadata, FsTreeNode.metadata), - ~active: option(Fp.t(Fp.absolute)), + ~active: option(FpExp.t(FpExp.absolute)), ~theme, ~decorations: Feature_Decorations.model, ~font: UiFont.t, @@ -136,14 +136,14 @@ let make = font iconTheme languageInfo - path={Fp.toString(data.path)} + path={FpExp.toString(data.path)} />, data, ) }; let decorations = Feature_Decorations.getDecorations( - ~path=Fp.toString(data.path), + ~path=FpExp.toString(data.path), decorations, ); { - switch (Fp.relativize(~source=base, ~dest=path)) { + switch (FpExp.relativize(~source=base, ~dest=path)) { | Ok(relativePath) => FpEx.explode(relativePath) |> List.map(hash) | Error(_) => [] }; @@ -25,27 +25,27 @@ module PathHasher = { let%test "equivalent paths" = { // TODO: Is this case even correct? - make(~base=Fp.(root), Fp.(root)) == []; + make(~base=FpExp.(root), FpExp.(root)) == []; }; let%test "simple path" = { - make(~base=Fp.(root), Fp.(At.(root / "abc"))) == [hash("abc")]; + make(~base=FpExp.(root), FpExp.(At.(root / "abc"))) == [hash("abc")]; }; let%test "multiple paths" = { - make(~base=Fp.(root), Fp.(At.(root / "abc" / "def"))) + make(~base=FpExp.(root), FpExp.(At.(root / "abc" / "def"))) == [hash("abc"), hash("def")]; }; }; let file = path => { - let basename = Fp.baseName(path) |> Option.value(~default="(empty)"); + let basename = FpExp.baseName(path) |> Option.value(~default="(empty)"); Tree.leaf({path, hash: PathHasher.hash(basename), displayName: basename}); }; let directory = (~isOpen=false, path, ~children) => { - let basename = Fp.baseName(path) |> Option.value(~default="(empty)"); + let basename = FpExp.baseName(path) |> Option.value(~default="(empty)"); Tree.node( ~expanded=isOpen, @@ -139,7 +139,10 @@ let replace = (~replacement, tree) => { }; loop( - PathHasher.make(~base=Fp.dirName(getPath(tree)), getPath(replacement)), + PathHasher.make( + ~base=FpExp.dirName(getPath(tree)), + getPath(replacement), + ), tree, ); }; @@ -167,7 +170,7 @@ let updateNodesInPath = | _ => node }; - loop(PathHasher.make(~base=Fp.dirName(getPath(tree)), path), tree); + loop(PathHasher.make(~base=FpExp.dirName(getPath(tree)), path), tree); }; let toggleOpen = diff --git a/src/Components/FileExplorer/FsTreeNode.rei b/src/Components/FileExplorer/FsTreeNode.rei index 8ebcc79752..61e5a928fe 100644 --- a/src/Components/FileExplorer/FsTreeNode.rei +++ b/src/Components/FileExplorer/FsTreeNode.rei @@ -2,7 +2,7 @@ open Oni_Core; [@deriving show({with_path: false})] type metadata = { - path: Fp.t(Fp.absolute), + path: FpExp.t(FpExp.absolute), displayName: string, hash: int // hash of basename, so only comparable locally }; @@ -10,18 +10,20 @@ type metadata = { [@deriving show({with_path: false})] type t = Tree.t(metadata, metadata); -let file: Fp.t(Fp.absolute) => t; -let directory: (~isOpen: bool=?, Fp.t(Fp.absolute), ~children: list(t)) => t; +let file: FpExp.t(FpExp.absolute) => t; +let directory: + (~isOpen: bool=?, FpExp.t(FpExp.absolute), ~children: list(t)) => t; -let getPath: t => Fp.t(Fp.absolute); +let getPath: t => FpExp.t(FpExp.absolute); let displayName: t => string; let findNodesByPath: - (Fp.t(Fp.absolute), t) => [ | `Success(list(t)) | `Partial(t) | `Failed]; -let findByPath: (Fp.t(Fp.absolute), t) => option(t); + (FpExp.t(FpExp.absolute), t) => + [ | `Success(list(t)) | `Partial(t) | `Failed]; +let findByPath: (FpExp.t(FpExp.absolute), t) => option(t); let replace: (~replacement: t, t) => t; -let updateNodesInPath: (t => t, Fp.t(Fp.absolute), t) => t; +let updateNodesInPath: (t => t, FpExp.t(FpExp.absolute), t) => t; let toggleOpen: t => t; let setOpen: t => t; diff --git a/src/Components/FileExplorer/Model.re b/src/Components/FileExplorer/Model.re index 3823ceaf68..238b8f6332 100644 --- a/src/Components/FileExplorer/Model.re +++ b/src/Components/FileExplorer/Model.re @@ -1,8 +1,9 @@ +open Oni_Core; // MODEL [@deriving show] type msg = - | ActiveFilePathChanged([@opaque] option(Fp.t(Fp.absolute))) + | ActiveFilePathChanged([@opaque] option(FpExp.t(FpExp.absolute))) | TreeLoaded(FsTreeNode.t) | TreeLoadError(string) | NodeLoaded(FsTreeNode.t) @@ -16,14 +17,14 @@ module Msg = { }; type model = { - rootPath: Fp.t(Fp.absolute), + rootPath: FpExp.t(FpExp.absolute), rootName: string, tree: option(FsTreeNode.t), treeView: Component_VimTree.model(FsTreeNode.metadata, FsTreeNode.metadata), isOpen: bool, scrollOffset: [ | `Start(float) | `Middle(float) | `Reveal(int)], - active: option(Fp.t(Fp.absolute)), - focus: option(Fp.t(Fp.absolute)) // path + active: option(FpExp.t(FpExp.absolute)), + focus: option(FpExp.t(FpExp.absolute)) // path }; let initial = (~rootPath) => { @@ -64,6 +65,5 @@ let getIndex = (path, model) => { }; let getFocusedIndex = model => { - model.active - |> Oni_Core.Utility.OptionEx.flatMap(path => getIndex(path, model)); + model.active |> Utility.OptionEx.flatMap(path => getIndex(path, model)); }; diff --git a/src/Core/Config.re b/src/Core/Config.re index d07c605716..1ad28415a8 100644 --- a/src/Core/Config.re +++ b/src/Core/Config.re @@ -41,10 +41,10 @@ module Settings = { }; let fromFile = path => - try(path |> Fp.toString |> Yojson.Safe.from_file |> fromJson) { + try(path |> FpExp.toString |> Yojson.Safe.from_file |> fromJson) { | Yojson.Json_error(message) => Log.errorf(m => - m("Failed to read file %s: %s", path |> Fp.toString, message) + m("Failed to read file %s: %s", path |> FpExp.toString, message) ); empty; }; diff --git a/src/Core/Config.rei b/src/Core/Config.rei index e852bd2707..2d06efc010 100644 --- a/src/Core/Config.rei +++ b/src/Core/Config.rei @@ -23,7 +23,7 @@ module Settings: { let fromList: list((string, Json.t)) => t; let fromJson: Json.t => t; - let fromFile: Fp.t(Fp.absolute) => t; + let fromFile: FpExp.t(FpExp.absolute) => t; let get: (key, t) => option(Json.t); diff --git a/src/Core/Filesystem.re b/src/Core/Filesystem.re index d116db44e9..ea3b93be9a 100644 --- a/src/Core/Filesystem.re +++ b/src/Core/Filesystem.re @@ -20,11 +20,11 @@ module Internal = { switch (Environment.os) { | Environment.Windows(_) => Sys.getenv_opt("LOCALAPPDATA") - |> OptionEx.flatMap(Fp.absoluteCurrentPlatform) + |> OptionEx.flatMap(FpExp.absoluteCurrentPlatform) |> Option.get | _ => switch (Sys.getenv_opt("HOME")) { - | Some(dir) => Fp.absoluteCurrentPlatform(dir) |> Option.get + | Some(dir) => FpExp.absoluteCurrentPlatform(dir) |> Option.get | None => failwith("Could not find user data directory") } } @@ -47,7 +47,7 @@ let userReadWriteExecute = 0o777; let stat = path => Unix.( - try(Some(stat(path |> Fp.toString))) { + try(Some(stat(path |> FpExp.toString))) { | Unix_error(ENOENT, _, _) => None } ); @@ -85,11 +85,11 @@ let isDir = path => */ let mkdir = (path, ~perm=userReadWriteExecute, ()) => Unix.( - try(mkdir(path |> Fp.toString, perm) |> return) { + try(mkdir(path |> FpExp.toString, perm) |> return) { | Unix_error(err, _, _) => error( "can't create directory '%s' because '%s", - path |> Fp.toString, + path |> FpExp.toString, error_message(err), ) } @@ -101,13 +101,13 @@ let mkdirp = path => { Ok(); } else if (isFile(curr)) { Error( - "Can't create directory because file exists: " ++ Fp.toString(curr), + "Can't create directory because file exists: " ++ FpExp.toString(curr), ); - } else if (!Fp.hasParentDir(curr)) { + } else if (!FpExp.hasParentDir(curr)) { Ok(); } else { // Create parent directory, if necesssary - loop(Fp.dirName(curr)) |> ResultEx.flatMap(mkdir(curr)); + loop(FpExp.dirName(curr)) |> ResultEx.flatMap(mkdir(curr)); }; loop(path) |> Result.map(_ => path); @@ -116,8 +116,8 @@ let mkdirp = path => { let getOniDirectory = dataDirectory => Revery.( switch (Environment.os) { - | Environment.Windows(_) => Fp.append(dataDirectory, "Oni2") |> return - | _ => Fp.At.(dataDirectory / ".config" / "oni2") |> return + | Environment.Windows(_) => FpExp.append(dataDirectory, "Oni2") |> return + | _ => FpExp.At.(dataDirectory / ".config" / "oni2") |> return } ); @@ -133,11 +133,11 @@ let getUserDataDirectory = () => let getOrCreateOniConfiguration = (~configDir, ~file) => { mkdirp(configDir) |> ResultEx.flatMap(_ => - if (isFile(Fp.append(configDir, file))) { + if (isFile(FpExp.append(configDir, file))) { // Already created Ok(); } else { - let userConfigPath = Fp.append(configDir, file) |> Fp.toString; + let userConfigPath = FpExp.append(configDir, file) |> FpExp.toString; let configFile = open_out(userConfigPath); let configString = @@ -162,37 +162,37 @@ let getOrCreateConfigFolder = mkdirp; let getExtensionsFolder = () => getUserDataDirectory() |> ResultEx.flatMap(getOniDirectory) - |> Result.map(dir => Fp.append(dir, "extensions")) + |> Result.map(dir => FpExp.append(dir, "extensions")) |> ResultEx.flatMap(mkdirp); let getStoreFolder = () => getUserDataDirectory() |> ResultEx.flatMap(getOniDirectory) - |> Result.map(dir => Fp.append(dir, "store")) + |> Result.map(dir => FpExp.append(dir, "store")) |> ResultEx.flatMap(mkdirp); let getGlobalStorageFolder = () => getUserDataDirectory() |> ResultEx.flatMap(getOniDirectory) - |> Result.map(dir => Fp.append(dir, "global")) + |> Result.map(dir => FpExp.append(dir, "global")) |> ResultEx.flatMap(mkdirp); let getWorkspaceStorageFolder = () => getUserDataDirectory() |> ResultEx.flatMap(getOniDirectory) - |> Result.map(dir => Fp.append(dir, "workspace")) + |> Result.map(dir => FpExp.append(dir, "workspace")) |> ResultEx.flatMap(mkdirp); let getSnippetsFolder = () => getUserDataDirectory() |> ResultEx.flatMap(getOniDirectory) - |> Result.map(dir => Fp.append(dir, "snippets")) + |> Result.map(dir => FpExp.append(dir, "snippets")) |> ResultEx.flatMap(mkdirp); let rec getOrCreateConfigFile = (~overridePath=?, filename) => { switch (overridePath) { | Some(path) => - let pathString = path |> Fp.toString; + let pathString = path |> FpExp.toString; switch (Sys.file_exists(pathString)) { | exception ex => Log.error("Error loading configuration file at: " ++ pathString); @@ -211,7 +211,7 @@ let rec getOrCreateConfigFile = (~overridePath=?, filename) => { |> ResultEx.flatMap(getOniDirectory) |> ResultEx.flatMap(configDir => getOrCreateOniConfiguration(~configDir, ~file=filename) - |> Result.map(() => Fp.append(configDir, filename)) + |> Result.map(() => FpExp.append(configDir, filename)) ) }; }; diff --git a/src/Core/Filesystem.rei b/src/Core/Filesystem.rei index d9269005ce..412c6b9dfc 100644 --- a/src/Core/Filesystem.rei +++ b/src/Core/Filesystem.rei @@ -1,20 +1,21 @@ type t('a) = result('a, string); -let getUserDataDirectory: unit => result(Fp.t(Fp.absolute), string); +let getUserDataDirectory: unit => result(FpExp.t(FpExp.absolute), string); -let getSnippetsFolder: unit => result(Fp.t(Fp.absolute), string); +let getSnippetsFolder: unit => result(FpExp.t(FpExp.absolute), string); -let getExtensionsFolder: unit => result(Fp.t(Fp.absolute), string); +let getExtensionsFolder: unit => result(FpExp.t(FpExp.absolute), string); -let getStoreFolder: unit => result(Fp.t(Fp.absolute), string); +let getStoreFolder: unit => result(FpExp.t(FpExp.absolute), string); -let getGlobalStorageFolder: unit => result(Fp.t(Fp.absolute), string); +let getGlobalStorageFolder: unit => result(FpExp.t(FpExp.absolute), string); -let getWorkspaceStorageFolder: unit => result(Fp.t(Fp.absolute), string); +let getWorkspaceStorageFolder: + unit => result(FpExp.t(FpExp.absolute), string); let getOrCreateConfigFolder: - Fp.t(Fp.absolute) => result(Fp.t(Fp.absolute), string); + FpExp.t(FpExp.absolute) => result(FpExp.t(FpExp.absolute), string); let getOrCreateConfigFile: - (~overridePath: Fp.t(Fp.absolute)=?, string) => - result(Fp.t(Fp.absolute), string); + (~overridePath: FpExp.t(FpExp.absolute)=?, string) => + result(FpExp.t(FpExp.absolute), string); diff --git a/src/Core/FpExp.re b/src/Core/FpExp.re new file mode 100644 index 0000000000..d5defa9819 --- /dev/null +++ b/src/Core/FpExp.re @@ -0,0 +1,201 @@ +// Experimental utilities on top of Fp - +// could potentially be merged into Fp when stable + +open Utility; + +type relative = Fp.relative; +type absolute = Fp.absolute; + +type platform = Fp.platform; + +type t('kind) = + | UNC({ + server: string, + share: string, + path: Fp.t(absolute), + }) + : t(absolute) + | Path(Fp.t('kind)): t('kind); + +module UNCParser = { + type t = { + server: string, + share: string, + path: string, + }; + + let parse = str => { + let len = String.length(str); + + if (len <= 3) { + None; + } else if (str.[0] == '\\' && str.[1] == '\\') { + // At least the start of a unc path... + let remainder = String.sub(str, 2, len - 2); + + switch (String.split_on_char('\\', remainder)) { + | [] => None + | [server, share, ...remainingElements] => + Some({server, share, path: String.concat("\\", remainingElements)}) + | _ => None + }; + } else { + None; + }; + }; + + let%test_module "UNC Parser" = + (module + { + let cases = [ + ("c:\\temp\\test-file.txt", None), + ( + "\\\\127.0.0.1\\c$\\temp\\test-file.txt", + Some({ + server: "127.0.0.1", + share: "c$", + path: "temp\\test-file.txt", + }), + ), + // "\\\\LOCALHOST\\c$\\temp\\test-file.txt", + // "\\\\.\\c:\\temp\\test-file.txt", + // "\\\\?\\c:\\temp\\test-file.txt", + // "\\\\.\\UNC\\LOCALHOST\\c$\\temp\\test-file.txt", + // "\\\\127.0.0.1\\c$\\temp\\test-file.txt", + ]; + let%test "unc paths should parse when platform is windows" = { + cases + |> List.for_all(((path, expected)) => parse(path) == expected); + }; + }); +}; + +let absolutePlatform = (~fromPlatform, str) => + // If we're on Windows - try parsing as a UNC path + if (fromPlatform == Fp.Windows(Win32)) { + str + |> UNCParser.parse + |> OptionEx.flatMap(({server, share, path}: UNCParser.t) => { + let fpPath = Fp.absolutePlatform(~fromPlatform, "\\" ++ path); + fpPath |> Option.map(path => {UNC({path, server, share})}); + }) + // If not a UNC path... switch back to + |> OptionEx.or_lazy(() => + str + |> Fp.absolutePlatform(~fromPlatform) + |> Option.map(path => Path(path)) + ); + } else { + str + |> Fp.absolutePlatform(~fromPlatform) + |> Option.map(path => Path(path)); + }; + +let root = Path(Fp.root); + +let absoluteCurrentPlatform = str => { + switch (Revery.Environment.os) { + | Revery.Environment.Windows(_) => + str |> absolutePlatform(~fromPlatform=Fp.Windows(Win32)) + | _ => str |> absolutePlatform(~fromPlatform=Fp.Posix) + }; +}; + +// TODO: Fix for UNC +let toString = + fun + | Path(path) => Fp.toString(path) + | UNC({path, server, share}) => + Printf.sprintf( + "\\\\%s\\%s%s", + server, + share, + Fp.toString(path) |> String.split_on_char('/') |> String.concat("\\"), + ); + +let%test_module "UNC Paths" = + (module + { + let fromPlatform = Fp.Windows(Win32); + let cases = [ + "\\\\127.0.0.1\\c$\\temp\\test-file.txt", + "\\\\LOCALHOST\\c$\\temp\\test-file.txt", + "\\\\.\\c:\\temp\\test-file.txt", + "\\\\?\\c:\\temp\\test-file.txt", + "\\\\.\\UNC\\LOCALHOST\\c$\\temp\\test-file.txt", + "\\\\127.0.0.1\\c$\\temp\\test-file.txt", + ]; + let%test "unc paths should parse when platform is windows" = { + cases + |> List.for_all(path => + absolutePlatform(~fromPlatform, path) |> Option.is_some + ); + }; + + let%test "unc paths round-trip with current brackets on windows" = { + cases + |> List.for_all(path => { + let actual = + absolutePlatform(~fromPlatform, path) |> Option.get |> toString; + String.equal(path, actual); + }); + }; + }); + +let isDescendent = (~ofPath, path) => { + switch (ofPath, path) { + | (UNC({path: ofPath, _}), UNC({path, _})) => + Fp.isDescendent(~ofPath, path) + | (Path(ofPath), Path(path)) => Fp.isDescendent(~ofPath, path) + | _ => false + }; +}; + +let relativize = (~source, ~dest) => { + switch (source, dest) { + | (UNC({path: source, _}), UNC({path: dest, _})) => + Fp.relativize(~source, ~dest) + | (Path(source), Path(dest)) => Fp.relativize(~source, ~dest) + | _ => Error(Invalid_argument("Not matching path types")) + }; +}; + +let baseName = + fun + | UNC({path, _}) + | Path(path) => Fp.baseName(path); + +let dirName = + fun + | UNC({path, server, share}) => + UNC({server, share, path: Fp.dirName(path)}) + | Path(path) => Path(Fp.dirName(path)); + +let eq = (pathA, pathB) => + switch (pathA, pathB) { + | ( + UNC({path: pathA, server: serverA, share: shareA}), + UNC({path: pathB, server: serverB, share: shareB}), + ) => + Fp.eq(pathA, pathB) + && String.equal(serverA, serverB) + && String.equal(shareA, shareB) + | (Path(pathA), Path(pathB)) => Fp.eq(pathA, pathB) + | _ => false + }; + +module At = { + let (/) = (path, name) => + switch (path) { + | UNC({path, server, share}) => + UNC({server, share, path: Fp.At.(path / name)}) + | Path(path) => Path(Fp.At.(path / name)) + }; +}; + +let hasParentDir = + fun + | UNC({path, _}) => Fp.hasParentDir(path) + | Path(path) => Fp.hasParentDir(path); + +let append = At.(/); diff --git a/src/Core/Oni_Core.re b/src/Core/Oni_Core.re index 5ce3fe47ea..319098a17e 100644 --- a/src/Core/Oni_Core.re +++ b/src/Core/Oni_Core.re @@ -26,6 +26,7 @@ module DiffMarkers = DiffMarkers; module EnvironmentVariables = Kernel.EnvironmentVariables; module Filesystem = Filesystem; module Filter = Filter; +module FpExp = FpExp; module Font = Font; module FontLigatures = FontLigatures; module FontSmoothing = FontSmoothing; diff --git a/src/Core/Persistence.re b/src/Core/Persistence.re index d5a51c3dfe..da71fecdd1 100644 --- a/src/Core/Persistence.re +++ b/src/Core/Persistence.re @@ -148,10 +148,10 @@ module Store = { }; let maybeFilePath = storeFolderResult - |> Result.map(storeFolder => Fp.append(storeFolder, hash)) + |> Result.map(storeFolder => FpExp.append(storeFolder, hash)) |> ResultEx.flatMap(Filesystem.getOrCreateConfigFolder) - |> Result.map(folder => Fp.append(folder, "store.json")) - |> Result.map(Fp.toString) + |> Result.map(folder => FpExp.append(folder, "store.json")) + |> Result.map(FpExp.toString) |> Result.fold( ~ok=Option.some, ~error=message => { diff --git a/src/Core/Persistence.rei b/src/Core/Persistence.rei index 18c498c25e..7005a84834 100644 --- a/src/Core/Persistence.rei +++ b/src/Core/Persistence.rei @@ -34,7 +34,7 @@ module Store: { let instantiate: ( - ~storeFolder: Fp.t(Fp.absolute)=?, + ~storeFolder: FpExp.t(FpExp.absolute)=?, string, unit => list(entry('state)) ) => diff --git a/src/Core/Uri.re b/src/Core/Uri.re index 5a6a1cf03c..cbf11f47ab 100644 --- a/src/Core/Uri.re +++ b/src/Core/Uri.re @@ -144,7 +144,7 @@ let fromPath = path => { }; let fromFilePath = fp => { - fp |> Fp.toString |> fromPath; + fp |> FpExp.toString |> fromPath; }; let toString = ({scheme, authority, path, query}: t) => { diff --git a/src/Core/Uri.rei b/src/Core/Uri.rei index b6602c3163..a99d00ca43 100644 --- a/src/Core/Uri.rei +++ b/src/Core/Uri.rei @@ -25,7 +25,7 @@ let of_yojson: Yojson.Safe.t => result(t, string); let fromMemory: string => t; let fromPath: string => t; -let fromFilePath: Fp.t(Fp.absolute) => t; +let fromFilePath: FpExp.t(FpExp.absolute) => t; let fromScheme: (~scheme: Scheme.t, ~authority: string=?, ~query: string=?, string) => t; diff --git a/src/Exthost/Extension/Exthost_Extension.rei b/src/Exthost/Extension/Exthost_Extension.rei index f965c5ec25..9c37643a57 100644 --- a/src/Exthost/Extension/Exthost_Extension.rei +++ b/src/Exthost/Extension/Exthost_Extension.rei @@ -1,3 +1,5 @@ +open Oni_Core; + module LocalizationDictionary: { type t; @@ -208,7 +210,8 @@ module Scanner: { }; let load: (~category: category, string) => option(ScanResult.t); - let scan: (~category: category, Fp.t(Fp.absolute)) => list(ScanResult.t); + let scan: + (~category: category, FpExp.t(FpExp.absolute)) => list(ScanResult.t); }; module InitData: { diff --git a/src/Exthost/Extension/Scanner.re b/src/Exthost/Extension/Scanner.re index e4fe58e5fd..32156ef0c1 100644 --- a/src/Exthost/Extension/Scanner.re +++ b/src/Exthost/Extension/Scanner.re @@ -71,8 +71,8 @@ let load = (~category, packageFile) => { }; }; -let scan = (~category, directory: Fp.t(Fp.absolute)) => { - let dirString = directory |> Fp.toString; +let scan = (~category, directory: FpExp.t(FpExp.absolute)) => { + let dirString = directory |> FpExp.toString; dirString |> Sys.readdir |> Array.to_list diff --git a/src/Feature/Explorer/Feature_Explorer.rei b/src/Feature/Explorer/Feature_Explorer.rei index b3c2576580..8451e17309 100644 --- a/src/Feature/Explorer/Feature_Explorer.rei +++ b/src/Feature/Explorer/Feature_Explorer.rei @@ -7,16 +7,16 @@ type msg; module Msg: { let keyPressed: string => msg; - let activeFileChanged: option(Fp.t(Fp.absolute)) => msg; + let activeFileChanged: option(FpExp.t(FpExp.absolute)) => msg; }; type model; -let initial: (~rootPath: option(Fp.t(Fp.absolute))) => model; +let initial: (~rootPath: option(FpExp.t(FpExp.absolute))) => model; -let setRoot: (~rootPath: option(Fp.t(Fp.absolute)), model) => model; +let setRoot: (~rootPath: option(FpExp.t(FpExp.absolute)), model) => model; -let root: model => option(Fp.t(Fp.absolute)); +let root: model => option(FpExp.t(FpExp.absolute)); let focusOutline: model => model; diff --git a/src/Feature/Extensions/Feature_Extensions.re b/src/Feature/Extensions/Feature_Extensions.re index 8c38978ae5..7c509c1387 100644 --- a/src/Feature/Extensions/Feature_Extensions.re +++ b/src/Feature/Extensions/Feature_Extensions.re @@ -83,7 +83,7 @@ let snippetFilePaths = (~fileType, model) => { } }) |> List.filter_map(({path, _}: Contributions.Snippet.t) => - Fp.absoluteCurrentPlatform(path) + FpExp.absoluteCurrentPlatform(path) ) ); }; diff --git a/src/Feature/Extensions/Feature_Extensions.rei b/src/Feature/Extensions/Feature_Extensions.rei index 4efe3542b8..dbfddb680f 100644 --- a/src/Feature/Extensions/Feature_Extensions.rei +++ b/src/Feature/Extensions/Feature_Extensions.rei @@ -39,7 +39,8 @@ let pick: (Exthost.Extension.Manifest.t => 'a, model) => list('a); let themeById: (~id: string, model) => option(Contributions.Theme.t); let themesByName: (~filter: string, model) => list(string); -let snippetFilePaths: (~fileType: string, model) => list(Fp.t(Fp.absolute)); +let snippetFilePaths: + (~fileType: string, model) => list(FpExp.t(FpExp.absolute)); let isBusy: model => bool; let isSearchInProgress: model => bool; @@ -72,7 +73,7 @@ let initial: ( ~workspacePersistence: Persistence.t, ~globalPersistence: Persistence.t, - ~extensionsFolder: option(Fp.t(Fp.absolute)) + ~extensionsFolder: option(FpExp.t(FpExp.absolute)) ) => model; diff --git a/src/Feature/Extensions/Model.re b/src/Feature/Extensions/Model.re index 600a48939e..f867ae7c29 100644 --- a/src/Feature/Extensions/Model.re +++ b/src/Feature/Extensions/Model.re @@ -256,7 +256,7 @@ type model = { extensions: list(Scanner.ScanResult.t), searchText: Component_InputText.model, latestQuery: option(Service_Extensions.Query.t), - extensionsFolder: option(Fp.t(Fp.absolute)), + extensionsFolder: option(FpExp.t(FpExp.absolute)), pendingInstalls: list(string), pendingUninstalls: list(string), globalValues: Yojson.Safe.t, diff --git a/src/Feature/Input/Feature_Input.rei b/src/Feature/Input/Feature_Input.rei index 18eca66114..03a407bbec 100644 --- a/src/Feature/Input/Feature_Input.rei +++ b/src/Feature/Input/Feature_Input.rei @@ -66,7 +66,7 @@ module KeybindingsLoader: { let none: t; - let file: Fp.t(Fp.absolute) => t; + let file: FpExp.t(FpExp.absolute) => t; }; [@deriving show] @@ -152,7 +152,7 @@ let remove: (uniqueId, model) => model; let enable: model => model; let disable: model => model; -let notifyFileSaved: (Fp.t(Fp.absolute), model) => model; +let notifyFileSaved: (FpExp.t(FpExp.absolute), model) => model; // UPDATE diff --git a/src/Feature/Input/KeybindingsLoader.re b/src/Feature/Input/KeybindingsLoader.re index 8979178d22..7a6bcef2b9 100644 --- a/src/Feature/Input/KeybindingsLoader.re +++ b/src/Feature/Input/KeybindingsLoader.re @@ -1,12 +1,14 @@ +open Oni_Core; + module Log = ( val Oni_Core.Log.withNamespace("Oni2.Feature.Input.KeybindingsLoader") ); module File = { - let loadKeybindings = (path: Fp.t(Fp.absolute)) => { + let loadKeybindings = (path: FpExp.t(FpExp.absolute)) => { let loadResult = path - |> Fp.toString + |> FpExp.toString |> Utility.JsonEx.from_file |> Utility.ResultEx.flatMap(Keybindings.of_yojson_with_errors); @@ -17,7 +19,7 @@ module File = { }; type params = { - filePath: Fp.t(Fp.absolute), + filePath: FpExp.t(FpExp.absolute), tick: int, }; // TODO: Once we've fixed the issue with the Service_OS.FileWatcher, @@ -32,11 +34,14 @@ module File = { let name = "Feature_Input.KeybindingsLoader.FileSubscription"; let id = ({filePath, tick}) => - Fp.toString(filePath) ++ string_of_int(tick); + FpExp.toString(filePath) ++ string_of_int(tick); let init = (~params, ~dispatch) => { Log.infof(m => - m("Reloading keybindings file: %s", Fp.toString(params.filePath)) + m( + "Reloading keybindings file: %s", + FpExp.toString(params.filePath), + ) ); dispatch(loadKeybindings(params.filePath)); (); @@ -57,7 +62,7 @@ module File = { type t = | None | File({ - filePath: Fp.t(Fp.absolute), + filePath: FpExp.t(FpExp.absolute), saveTick: int, }); @@ -69,7 +74,7 @@ let notifyFileSaved = path => fun | None => None | File({filePath, saveTick}) as orig => - if (Fp.eq(filePath, path)) { + if (FpExp.eq(filePath, path)) { File({filePath, saveTick: saveTick + 1}); } else { orig; diff --git a/src/Feature/LanguageSupport/CompletionProvider.re b/src/Feature/LanguageSupport/CompletionProvider.re index ca257ee8b2..a7c8f38ee8 100644 --- a/src/Feature/LanguageSupport/CompletionProvider.re +++ b/src/Feature/LanguageSupport/CompletionProvider.re @@ -297,7 +297,7 @@ let keyword: provider(keywordModel, keywordMsg) = { type snippetModel = { items: list(CompletionItem.t), isComplete: bool, - filePaths: list(Fp.t(Fp.absolute)), + filePaths: list(FpExp.t(FpExp.absolute)), fileType: string, sortOrder: [ | `Top | `Inline | `Bottom | `Hidden], }; diff --git a/src/Feature/Snippets/Feature_Snippets.re b/src/Feature/Snippets/Feature_Snippets.re index a077eeeeb8..b09a60b857 100644 --- a/src/Feature/Snippets/Feature_Snippets.re +++ b/src/Feature/Snippets/Feature_Snippets.re @@ -351,7 +351,7 @@ type msg = | EditSnippetFileRequested({ snippetFile: Service_Snippets.SnippetFileMetadata.t, }) - | SnippetFileCreatedSuccessfully([@opaque] Fp.t(Fp.absolute)) + | SnippetFileCreatedSuccessfully([@opaque] FpExp.t(FpExp.absolute)) | SnippetFileCreationError(string); module Msg = { @@ -393,7 +393,7 @@ type outmsg = | SetSelections(list(ByteRange.t)) | ShowPicker(list(Service_Snippets.SnippetWithMetadata.t)) | ShowFilePicker(list(Service_Snippets.SnippetFileMetadata.t)) - | OpenFile(Fp.t(Fp.absolute)) + | OpenFile(FpExp.t(FpExp.absolute)) | Nothing; module Effects = { diff --git a/src/Feature/Snippets/Feature_Snippets.rei b/src/Feature/Snippets/Feature_Snippets.rei index 4031dbe637..9e02c907d0 100644 --- a/src/Feature/Snippets/Feature_Snippets.rei +++ b/src/Feature/Snippets/Feature_Snippets.rei @@ -21,7 +21,7 @@ type outmsg = | SetSelections(list(ByteRange.t)) | ShowPicker(list(Service_Snippets.SnippetWithMetadata.t)) | ShowFilePicker(list(Service_Snippets.SnippetFileMetadata.t)) - | OpenFile(Fp.t(Fp.absolute)) + | OpenFile(FpExp.t(FpExp.absolute)) | Nothing; module Session: { diff --git a/src/Feature/Workspace/Feature_Workspace.re b/src/Feature/Workspace/Feature_Workspace.re index d4455f135a..28bf54f68a 100644 --- a/src/Feature/Workspace/Feature_Workspace.re +++ b/src/Feature/Workspace/Feature_Workspace.re @@ -21,7 +21,7 @@ type command = type msg = | Command(command) | FolderSelectionCanceled - | FolderPicked([@opaque] Fp.t(Fp.absolute)) + | FolderPicked([@opaque] FpExp.t(FpExp.absolute)) | WorkingDirectoryChanged(string) | Noop; @@ -54,10 +54,10 @@ type outmsg = | WorkspaceChanged(option(string)); module Effects = { - let changeDirectory = (path: Fp.t(Fp.absolute)) => + let changeDirectory = (path: FpExp.t(FpExp.absolute)) => Isolinear.Effect.createWithDispatch( ~name="Feature_Workspace.changeDirectory", dispatch => { - let newDirectory = Fp.toString(path); + let newDirectory = FpExp.toString(path); switch (Luv.Path.chdir(newDirectory)) { | Ok () => dispatch(WorkingDirectoryChanged(newDirectory)) | Error(msg) => @@ -131,7 +131,7 @@ module Commands = { |> Json.Decode.decode_value(Uri.decode) |> Result.map(Uri.toFileSystemPath) |> Result.to_option - |> OptionEx.flatMap(Fp.absoluteCurrentPlatform) + |> OptionEx.flatMap(FpExp.absoluteCurrentPlatform) |> Option.map(fp => FolderPicked(fp)) |> Option.value(~default=Noop) | _ => Noop, diff --git a/src/Feature/Workspace/Feature_Workspace.rei b/src/Feature/Workspace/Feature_Workspace.rei index 448667d209..3854f43f61 100644 --- a/src/Feature/Workspace/Feature_Workspace.rei +++ b/src/Feature/Workspace/Feature_Workspace.rei @@ -29,7 +29,7 @@ let update: (msg, model) => (model, outmsg); // EFFECTS module Effects: { - let changeDirectory: Fp.t(Fp.absolute) => Isolinear.Effect.t(msg); + let changeDirectory: FpExp.t(FpExp.absolute) => Isolinear.Effect.t(msg); let pickFolder: Isolinear.Effect.t(msg); }; diff --git a/src/Model/State.re b/src/Model/State.re index e6368d45fe..13c55716ba 100644 --- a/src/Model/State.re +++ b/src/Model/State.re @@ -560,7 +560,7 @@ let initial = titlebarHeight, workspace: Feature_Workspace.initial( - ~openedFolder=maybeWorkspace |> Option.map(Fp.toString), + ~openedFolder=maybeWorkspace |> Option.map(FpExp.toString), workingDirectory, ), fileExplorer: Feature_Explorer.initial(~rootPath=maybeWorkspace), diff --git a/src/Service/Extensions/Management.re b/src/Service/Extensions/Management.re index 1095cd08a7..a3584c7166 100644 --- a/src/Service/Extensions/Management.re +++ b/src/Service/Extensions/Management.re @@ -22,7 +22,7 @@ module Internal = { |> Option.map( FunEx.tap(p => Log.infof(m => - m("Searching for user extensions in: %s", p |> Fp.toString) + m("Searching for user extensions in: %s", p |> FpExp.toString) ) ), ) @@ -48,14 +48,14 @@ module Internal = { m( "Installing extension %s to %s", name, - extensionsFolder |> Fp.toString, + extensionsFolder |> FpExp.toString, ) ); NodeTask.run( ~name="Install", ~setup, - ~args=[absolutePath, extensionsFolder |> Fp.toString, folderName], + ~args=[absolutePath, extensionsFolder |> FpExp.toString, folderName], "install-extension.js", ); }; diff --git a/src/Service/Extensions/Service_Extensions.rei b/src/Service/Extensions/Service_Extensions.rei index 731d768c81..77fcc1b3f5 100644 --- a/src/Service/Extensions/Service_Extensions.rei +++ b/src/Service/Extensions/Service_Extensions.rei @@ -89,14 +89,14 @@ module Catalog: { module Management: { let install: - (~setup: Setup.t, ~extensionsFolder: Fp.t(Fp.absolute)=?, string) => + (~setup: Setup.t, ~extensionsFolder: FpExp.t(FpExp.absolute)=?, string) => Lwt.t(unit); let uninstall: - (~extensionsFolder: Fp.t(Fp.absolute)=?, string) => Lwt.t(unit); + (~extensionsFolder: FpExp.t(FpExp.absolute)=?, string) => Lwt.t(unit); let get: - (~extensionsFolder: Fp.t(Fp.absolute)=?, unit) => + (~extensionsFolder: FpExp.t(FpExp.absolute)=?, unit) => Lwt.t(list(Exthost.Extension.Scanner.ScanResult.t)); }; @@ -114,7 +114,7 @@ module Query: { module Effects: { let uninstall: ( - ~extensionsFolder: option(Fp.t(Fp.absolute)), + ~extensionsFolder: option(FpExp.t(FpExp.absolute)), ~toMsg: result(unit, string) => 'a, string ) => @@ -122,7 +122,7 @@ module Effects: { let install: ( - ~extensionsFolder: option(Fp.t(Fp.absolute)), + ~extensionsFolder: option(FpExp.t(FpExp.absolute)), ~toMsg: result(Exthost.Extension.Scanner.ScanResult.t, string) => 'a, string ) => @@ -130,7 +130,7 @@ module Effects: { let update: ( - ~extensionsFolder: option(Fp.t(Fp.absolute)), + ~extensionsFolder: option(FpExp.t(FpExp.absolute)), ~toMsg: result(Exthost.Extension.Scanner.ScanResult.t, string) => 'msg, string ) => diff --git a/src/Service/OS/Service_OS.re b/src/Service/OS/Service_OS.re index 836eca91fc..a4f0b23064 100644 --- a/src/Service/OS/Service_OS.re +++ b/src/Service/OS/Service_OS.re @@ -282,7 +282,10 @@ module Effect = { let selectedFolders = maybeFolders |> Option.value(~default=[||]); if (Array.length(selectedFolders) > 0) { - selectedFolders[0] |> Fp.absoluteCurrentPlatform |> toMsg |> dispatch; + selectedFolders[0] + |> FpExp.absoluteCurrentPlatform + |> toMsg + |> dispatch; } else { None |> toMsg |> dispatch; }; diff --git a/src/Service/OS/Service_OS.rei b/src/Service/OS/Service_OS.rei index 2ba53de775..7f4a1df9ce 100644 --- a/src/Service/OS/Service_OS.rei +++ b/src/Service/OS/Service_OS.rei @@ -1,3 +1,5 @@ +open Oni_Core; + module Api: { let fold: ( @@ -38,7 +40,10 @@ module Effect: { module Dialog: { let openFolder: - (~initialDirectory: string=?, option(Fp.t(Fp.absolute)) => 'msg) => + ( + ~initialDirectory: string=?, + option(FpExp.t(FpExp.absolute)) => 'msg + ) => Isolinear.Effect.t('msg); }; }; diff --git a/src/Service/Snippets/Service_Snippets.re b/src/Service/Snippets/Service_Snippets.re index 766d8e57e3..41ec14ac7f 100644 --- a/src/Service/Snippets/Service_Snippets.re +++ b/src/Service/Snippets/Service_Snippets.re @@ -25,7 +25,7 @@ module SnippetFileMetadata = { [@deriving show] type t = { language: option(string), - filePath: [@opaque] Fp.t(Fp.absolute), + filePath: [@opaque] FpExp.t(FpExp.absolute), isCreated: bool, }; }; @@ -137,12 +137,12 @@ module Internal = { let readSnippetFilesFromFolder = folder => { folder - |> Fp.toString + |> FpExp.toString |> Service_OS.Api.readdir |> Lwt.map(dirents => { dirents |> List.map((dirent: Luv.File.Dirent.t) => - Fp.At.(folder / dirent.name) + FpExp.At.(folder / dirent.name) ) |> List.filter(file => SnippetFile.scope(file) != None) }); @@ -153,8 +153,8 @@ module Internal = { |> LwtEx.flatMap(snippetFiles => { snippetFiles |> List.filter(SnippetFile.matches(~fileType)) - |> List.map((dir: Fp.t(Fp.absolute)) => { - let str = Fp.toString(dir); + |> List.map((dir: FpExp.t(FpExp.absolute)) => { + let str = FpExp.toString(dir); Cache.get(str) |> Lwt.map( List.filter( @@ -169,7 +169,8 @@ module Internal = { let loadSnippetsFromFiles = (~filePaths, ~fileType, dispatch) => { // Load all files // Coalesce all promises - let promises = filePaths |> List.map(Fp.toString) |> List.map(Cache.get); + let promises = + filePaths |> List.map(FpExp.toString) |> List.map(Cache.get); let userPromise: Lwt.t(list(SnippetWithMetadata.t)) = Filesystem.getSnippetsFolder() @@ -213,9 +214,9 @@ module Effect = { let clearCachedSnippets = (~filePath) => { Isolinear.Effect.create(~name="Service_Snippets.clearCachedSnippets", () => { Log.tracef(m => - m("Clearing snippet cache for file: %s", filePath |> Fp.toString) + m("Clearing snippet cache for file: %s", filePath |> FpExp.toString) ); - Cache.clear(Fp.toString(filePath)); + Cache.clear(FpExp.toString(filePath)); }); }; let snippetFromFiles = (~fileType, ~filePaths, toMsg) => @@ -315,7 +316,7 @@ module Effect = { module Sub = { type snippetFileParams = { uniqueId: string, - filePaths: list(Fp.t(Fp.absolute)), + filePaths: list(FpExp.t(FpExp.absolute)), fileType: string, }; module SnippetFileSubscription = diff --git a/src/Service/Snippets/Service_Snippets.rei b/src/Service/Snippets/Service_Snippets.rei index 8ab6fd7f2e..4491ae111e 100644 --- a/src/Service/Snippets/Service_Snippets.rei +++ b/src/Service/Snippets/Service_Snippets.rei @@ -2,6 +2,7 @@ // 1) Load snippets from path (implement snippetFromFiles) // 2) Add placeholder text for snippet file, chain up open // 3) Expand `snippetFromFiles` to include user snippets, too +open Oni_Core; module SnippetWithMetadata: { [@deriving show] @@ -17,7 +18,7 @@ module SnippetFileMetadata: { [@deriving show] type t = { language: option(string), - filePath: Fp.t(Fp.absolute), + filePath: FpExp.t(FpExp.absolute), isCreated: bool, }; }; @@ -25,18 +26,18 @@ module SnippetFileMetadata: { module Effect: { let createSnippetFile: ( - ~filePath: Fp.t(Fp.absolute), - result(Fp.t(Fp.absolute), string) => 'msg + ~filePath: FpExp.t(FpExp.absolute), + result(FpExp.t(FpExp.absolute), string) => 'msg ) => Isolinear.Effect.t('msg); let clearCachedSnippets: - (~filePath: Fp.t(Fp.absolute)) => Isolinear.Effect.t(_); + (~filePath: FpExp.t(FpExp.absolute)) => Isolinear.Effect.t(_); let snippetFromFiles: ( ~fileType: string, - ~filePaths: list(Fp.t(Fp.absolute)), + ~filePaths: list(FpExp.t(FpExp.absolute)), list(SnippetWithMetadata.t) => 'msg ) => Isolinear.Effect.t('msg); @@ -54,7 +55,7 @@ module Sub: { ( ~uniqueId: string, ~fileType: string, - ~filePaths: list(Fp.t(Fp.absolute)), + ~filePaths: list(FpExp.t(FpExp.absolute)), list(SnippetWithMetadata.t) => 'msg ) => Isolinear.Sub.t('msg); diff --git a/src/Service/Snippets/SnippetFile.re b/src/Service/Snippets/SnippetFile.re index 8a3b85611f..5e703ab32a 100644 --- a/src/Service/Snippets/SnippetFile.re +++ b/src/Service/Snippets/SnippetFile.re @@ -1,21 +1,23 @@ -type t = Fp.t(Fp.absolute); +open Oni_Core; + +type t = FpExp.t(Fp.absolute); type scope = | Global | Language(string); -let global = (folder: Fp.t(Fp.absolute)) => { - Fp.At.(folder / "global.code-snippets"); +let global = (folder: FpExp.t(Fp.absolute)) => { + FpExp.At.(folder / "global.code-snippets"); }; -let language = (~fileType: string, folder: Fp.t(Fp.absolute)) => { +let language = (~fileType: string, folder: FpExp.t(Fp.absolute)) => { let fileName = fileType ++ ".json"; - Fp.At.(folder / fileName); + FpExp.At.(folder / fileName); }; let scope = (path: t) => { let splitPath = - path |> Fp.toString |> Filename.basename |> String.split_on_char('.'); + path |> FpExp.toString |> Filename.basename |> String.split_on_char('.'); switch (splitPath) { | [] => None @@ -89,7 +91,7 @@ let ensureCreated = (snippetFile: t) => { Lwt.catch( () => { snippetFile - |> Fp.toString + |> FpExp.toString |> Service_OS.Api.stat |> Lwt.map(_ => snippetFile) }, @@ -108,7 +110,10 @@ let ensureCreated = (snippetFile: t) => { Lwt.catch( () => { // File not created yet, let's create it - Service_OS.Api.writeFile(~contents, Fp.toString(snippetFile)) + Service_OS.Api.writeFile( + ~contents, + FpExp.toString(snippetFile), + ) |> Lwt.map(() => snippetFile) }, exn => Lwt.fail_with(Printexc.to_string(exn)), @@ -117,7 +122,7 @@ let ensureCreated = (snippetFile: t) => { |> Option.value( ~default= Lwt.fail_with( - "Invalid snippet file path: " ++ Fp.toString(snippetFile), + "Invalid snippet file path: " ++ FpExp.toString(snippetFile), ), ); }, diff --git a/src/Store/ConfigurationStoreConnector.re b/src/Store/ConfigurationStoreConnector.re index 7cb0c003a0..45576d7eb5 100644 --- a/src/Store/ConfigurationStoreConnector.re +++ b/src/Store/ConfigurationStoreConnector.re @@ -15,7 +15,7 @@ module Constants = { let start = ( - ~configurationFilePath: option(Fp.t(Fp.absolute)), + ~configurationFilePath: option(FpExp.t(FpExp.absolute)), ~setVsync, ~shouldLoadConfiguration, ~filesToOpen, @@ -87,7 +87,7 @@ let start = dispatch(Actions.Configuration(UserSettingsChanged)); defaultConfigurationFileName |> getConfigurationFile - |> Result.map(Fp.toString) + |> Result.map(FpExp.toString) |> ( result => Stdlib.Result.bind(result, ConfigurationParser.ofFile) @@ -109,8 +109,8 @@ let start = Log.error("Unable to load configuration: " ++ msg); Isolinear.Effect.none; | Ok(configPath) => - // TODO: Fp.t all the way... - let configPath = Fp.toString(configPath); + // TODO: FpExp.t all the way... + let configPath = FpExp.toString(configPath); if (!Feature_Buffers.isModifiedByPath(buffers, configPath)) { Oni_Core.Log.perf("Apply configuration transform", () => { let parsedJson = Yojson.Safe.from_file(configPath); @@ -140,7 +140,7 @@ let start = defaultConfigurationFileName |> getConfigurationFile - |> Result.map(Fp.toString) + |> Result.map(FpExp.toString) // Once we know the path - register a listener to reload |> ResultEx.tap(configPath => reloadConfigOnWritePost(~configPath, dispatch) @@ -173,7 +173,7 @@ let start = ~name="configuration.openFile", dispatch => switch (Filesystem.getOrCreateConfigFile(filePath)) { | Ok(path) => - dispatch(Actions.OpenFileByPath(path |> Fp.toString, None, None)) + dispatch(Actions.OpenFileByPath(path |> FpExp.toString, None, None)) | Error(e) => onError(~dispatch, e) } ); diff --git a/src/Store/Features.re b/src/Store/Features.re index 86acc434e4..3f69c33cba 100644 --- a/src/Store/Features.re +++ b/src/Store/Features.re @@ -92,7 +92,7 @@ module Internal = { dispatch(Actions.Quit(true)) ); - let chdir = (path: Fp.t(Fp.absolute)) => + let chdir = (path: FpExp.t(FpExp.absolute)) => Feature_Workspace.Effects.changeDirectory(path) |> Isolinear.Effect.map(msg => Actions.Workspace(msg)); @@ -1218,7 +1218,7 @@ let update = let maybeFullPath = buffer |> Buffer.getFilePath - |> OptionEx.flatMap(Fp.absoluteCurrentPlatform); + |> OptionEx.flatMap(FpExp.absoluteCurrentPlatform); let clearSnippetCacheEffect = maybeFullPath @@ -1824,7 +1824,7 @@ let update = | OpenFile(filePath) => ( state.layout, - Internal.openFileEffect(Fp.toString(filePath)), + Internal.openFileEffect(FpExp.toString(filePath)), ) | Effect(eff) => ( @@ -2031,7 +2031,8 @@ let update = | WorkspaceChanged(maybeWorkspaceFolder) => let maybeExplorerFolder = - maybeWorkspaceFolder |> OptionEx.flatMap(Fp.absoluteCurrentPlatform); + maybeWorkspaceFolder + |> OptionEx.flatMap(FpExp.absoluteCurrentPlatform); let fileExplorer = Feature_Explorer.setRoot( ~rootPath=maybeExplorerFolder, diff --git a/src/Store/QuickmenuStoreConnector.re b/src/Store/QuickmenuStoreConnector.re index de67a50752..2f33377d1d 100644 --- a/src/Store/QuickmenuStoreConnector.re +++ b/src/Store/QuickmenuStoreConnector.re @@ -276,7 +276,7 @@ let start = () => { snippetFiles |> List.filter_map( (snippetFile: Service_Snippets.SnippetFileMetadata.t) => { - Fp.baseName(snippetFile.filePath) + FpExp.baseName(snippetFile.filePath) |> Option.map(filePath => { Actions.{ category: diff --git a/src/Store/StoreThread.re b/src/Store/StoreThread.re index bd79173460..ac4fa1cad9 100644 --- a/src/Store/StoreThread.re +++ b/src/Store/StoreThread.re @@ -8,6 +8,7 @@ */ module Core = Oni_Core; +module FpExp = Oni_Core.FpExp; module Model = Oni_Model; @@ -23,7 +24,7 @@ let discoverExtensions = Core.Log.perf("Discover extensions", () => { let extensions = setup.bundledExtensionsPath - |> Fp.absoluteCurrentPlatform + |> FpExp.absoluteCurrentPlatform |> Option.map( Scanner.scan( // The extension host assumes bundled extensions start with 'vscode.' @@ -34,7 +35,7 @@ let discoverExtensions = let developmentExtensions = setup.developmentExtensionsPath - |> Core.Utility.OptionEx.flatMap(Fp.absoluteCurrentPlatform) + |> Core.Utility.OptionEx.flatMap(FpExp.absoluteCurrentPlatform) |> Option.map(Scanner.scan(~category=Development)) |> Option.value(~default=[]); @@ -349,7 +350,7 @@ let start = ~toMsg=maybeFilePathStr => { let maybeFilePath = maybeFilePathStr - |> Utility.OptionEx.flatMap(Fp.absoluteCurrentPlatform); + |> Utility.OptionEx.flatMap(FpExp.absoluteCurrentPlatform); Model.Actions.FileExplorer( Feature_Explorer.Msg.activeFileChanged(maybeFilePath), ); diff --git a/src/bin_editor/Oni2_editor.re b/src/bin_editor/Oni2_editor.re index 8555637233..21b0e497d8 100644 --- a/src/bin_editor/Oni2_editor.re +++ b/src/bin_editor/Oni2_editor.re @@ -10,6 +10,7 @@ open Oni_CLI; open Oni_UI; module Core = Oni_Core; +module FpExp = Oni_Core.FpExp; module Input = Oni_Input; module Model = Oni_Model; module Store = Oni_Store; @@ -119,7 +120,7 @@ switch (eff) { // The directory that was persisted is a valid workspace, so we can use it if (couldChangeDirectory^) { - maybePath |> OptionEx.flatMap(Fp.absoluteCurrentPlatform); + maybePath |> OptionEx.flatMap(FpExp.absoluteCurrentPlatform); } else { None; }; @@ -142,7 +143,7 @@ switch (eff) { Store.Persistence.Workspace.( maybeWorkspace |> Option.map(workspace => { - let store = storeFor(Fp.toString(workspace)); + let store = storeFor(FpExp.toString(workspace)); ( windowX(store) |> OptionEx.tap(x => @@ -246,7 +247,7 @@ switch (eff) { let maybeWorkspace = initWorkspace(); let workingDirectory = maybeWorkspace - |> Option.map(Fp.toString) + |> Option.map(FpExp.toString) |> Option.value(~default=initialWorkingDirectory); let window = @@ -320,7 +321,7 @@ switch (eff) { ~contributedCommands=[], // TODO ~workingDirectory, ~maybeWorkspace, - // TODO: Use `Fp.t` all the way down + // TODO: Use `FpExp.t` all the way down ~extensionsFolder=cliOptions.overriddenExtensionsDir, ~licenseKeyPersistence, ~titlebarHeight=Revery.Window.getTitlebarHeight(window), diff --git a/test/Core/PersistenceTests.re b/test/Core/PersistenceTests.re index 9c36d13792..10ec8d0531 100644 --- a/test/Core/PersistenceTests.re +++ b/test/Core/PersistenceTests.re @@ -6,7 +6,7 @@ module Store = Persistence.Store; open Persistence.Schema; type testContext = { - storeFolder: Fp.t(Fp.absolute), + storeFolder: FpExp.t(FpExp.absolute), store: Store.t(bool), testBool: item(bool, bool), }; @@ -27,10 +27,11 @@ describe("Persistence", ({test, _}) => { storeFolderTemplate |> Luv.File.Sync.mkdtemp |> Result.to_option - |> OptionEx.flatMap(Fp.absoluteCurrentPlatform) + |> OptionEx.flatMap(FpExp.absoluteCurrentPlatform) |> Option.get; prerr_endline( - "Persistence.setup - created storeFolder: " ++ Fp.toString(storeFolder), + "Persistence.setup - created storeFolder: " + ++ FpExp.toString(storeFolder), ); let instantiate = Store.instantiate(~storeFolder); @@ -52,7 +53,7 @@ describe("Persistence", ({test, _}) => { // We'll write out an empty file... let filePath = - Fp.At.(storeFolder / "global" / "store.json") |> Fp.toString; + FpExp.At.(storeFolder / "global" / "store.json") |> FpExp.toString; let oc = open_out(filePath); Printf.fprintf(oc, "\n"); close_out(oc); diff --git a/test/Service/Extensions/ManagementTests.re b/test/Service/Extensions/ManagementTests.re index bbf3c5df34..86116edeb6 100644 --- a/test/Service/Extensions/ManagementTests.re +++ b/test/Service/Extensions/ManagementTests.re @@ -15,7 +15,7 @@ let createExtensionsFolder = () => Service_OS.Api.mktempdir(~prefix="extensions-test", ()) |> LwtEx.sync |> Result.to_option - |> OptionEx.flatMap(Fp.absoluteCurrentPlatform) + |> OptionEx.flatMap(FpExp.absoluteCurrentPlatform) |> Option.get; let setup = Setup.init();