1
1
mirror of https://github.com/qvacua/vimr.git synced 2024-11-28 02:54:31 +03:00

Merge remote-tracking branch 'origin/develop' into update-neovim

Conflicts:
	Cartfile
	Cartfile.resolved
This commit is contained in:
Tae Won Ha 2018-07-09 21:54:29 +02:00
commit 2caeb2c10e
51 changed files with 1528 additions and 1063 deletions

View File

@ -1,14 +1,14 @@
github "ReactiveX/RxSwift" "4.1.2"
github "ReactiveX/RxSwift" "4.2.0"
github "PureLayout/PureLayout" == 3.0.2
github "eonil/FileSystemEvents" == 1.0.0
github "sparkle-project/Sparkle" == 1.19.0
github "qvacua/CocoaFontAwesome" "master"
github "qvacua/CocoaMarkdown" "master"
github "qvacua/RxMsgpackRpc" == 0.0.1
github "qvacua/RxMessagePort" == 0.0.1
github "qvacua/RxMsgpackRpc" == 0.0.2
github "qvacua/RxMessagePort" == 0.0.2
github "qvacua/RxNeovimApi" "develop"
github "sindresorhus/github-markdown-css" == 2.10.0
github "httpswift/swifter" == 1.4.1
github "httpswift/swifter" == 1.4.2
github "a2/MessagePack.swift" == 3.0.0
github "Quick/Nimble" == 7.1.2

View File

@ -1,13 +1,13 @@
github "PureLayout/PureLayout" "v3.0.2"
github "Quick/Nimble" "v7.1.2"
github "ReactiveX/RxSwift" "4.1.2"
github "ReactiveX/RxSwift" "4.2.0"
github "a2/MessagePack.swift" "3.0.0"
github "eonil/FileSystemEvents" "1.0.0"
github "httpswift/swifter" "1.4.1"
github "httpswift/swifter" "1.4.2"
github "qvacua/CocoaFontAwesome" "76cf6c4ef3088d84f78988183c56fc6abdc19f83"
github "qvacua/CocoaMarkdown" "7756ad96d5fb390c66531004868e828bb54d3609"
github "qvacua/RxMessagePort" "v0.0.1"
github "qvacua/RxMsgpackRpc" "v0.0.1"
github "qvacua/RxNeovimApi" "c21467e1442326bdf4715539300f315947f49e64"
github "qvacua/RxMessagePort" "v0.0.2"
github "qvacua/RxMsgpackRpc" "v0.0.2"
github "qvacua/RxNeovimApi" "640236a82d195f060a7964dd3ea572e614f18191"
github "sindresorhus/github-markdown-css" "v2.10.0"
github "sparkle-project/Sparkle" "1.19.0"

View File

@ -28,7 +28,6 @@
#import <nvim/syntax.h>
#import <nvim/aucmd.h>
#import <nvim/msgpack_rpc/helpers.h>
#import <msgpack.h>
#import <nvim/api/private/helpers.h>
@ -236,7 +235,9 @@ static void run_neovim(void *arg) {
argv[0] = "nvim";
for (var i = 0; i < nvimArgs.count; i++) {
argv[i + 1] = (char *) nvimArgs[(NSUInteger) i].cstr;
char *str = (char *) nvimArgs[(NSUInteger) i].cstr;
argv[i + 1] = malloc(strlen(str) + 1);
strcpy(argv[i + 1], str);
}
[nvimArgs release]; // retained in start_neovim()
@ -244,6 +245,9 @@ static void run_neovim(void *arg) {
nvim_main(argc, argv);
for (var i = 0; i < argc - 1; i++) {
free(argv[i + 1]);
}
free(argv);
}

View File

@ -13,6 +13,7 @@
1929B8CA73D3702364903BB7 /* SimpleCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BD86668017F804408E3A /* SimpleCache.swift */; };
1929BA70C221E3C199833B8C /* UiBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B52174EC68D2974B5BAE /* UiBridge.swift */; };
1929BA93BDEA029011F034FF /* RxSwiftCommons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B6F4B70B90F7CFB7B523 /* RxSwiftCommons.swift */; };
1929BCA615324C58582BFC3C /* ProcessUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BD167BE7C6BB788DAE2A /* ProcessUtils.swift */; };
4B177886201220F300E32FF0 /* SharedTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 1929B4F32708E99C40A57020 /* SharedTypes.h */; settings = {ATTRIBUTES = (Public, ); }; };
4B17E549209E3E4100265C1D /* RxNeovimApi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B17E548209E3E4100265C1D /* RxNeovimApi.framework */; };
4B8662E81FDC3F9F007F490D /* vimr.vim in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4B8662E41FDC3D4F007F490D /* vimr.vim */; };
@ -100,6 +101,7 @@
1929B52174EC68D2974B5BAE /* UiBridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UiBridge.swift; sourceTree = "<group>"; };
1929B6F4B70B90F7CFB7B523 /* RxSwiftCommons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RxSwiftCommons.swift; sourceTree = "<group>"; };
1929BBD7F88AE4F01E626691 /* NvimApiExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NvimApiExtension.swift; sourceTree = "<group>"; };
1929BD167BE7C6BB788DAE2A /* ProcessUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProcessUtils.swift; sourceTree = "<group>"; };
1929BD86668017F804408E3A /* SimpleCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleCache.swift; sourceTree = "<group>"; };
4B17E548209E3E4100265C1D /* RxNeovimApi.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxNeovimApi.framework; path = ../Carthage/Build/Mac/RxNeovimApi.framework; sourceTree = "<group>"; };
4B8662E41FDC3D4F007F490D /* vimr.vim */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = vimr.vim; sourceTree = "<group>"; };
@ -228,6 +230,7 @@
1929B6F4B70B90F7CFB7B523 /* RxSwiftCommons.swift */,
1929B44323D6611E2927EC3B /* MessagePackCommons.swift */,
1929BD86668017F804408E3A /* SimpleCache.swift */,
1929BD167BE7C6BB788DAE2A /* ProcessUtils.swift */,
);
path = NvimView;
sourceTree = "<group>";
@ -409,6 +412,7 @@
1929BA93BDEA029011F034FF /* RxSwiftCommons.swift in Sources */,
1929B30D6C4175835D1F5B21 /* MessagePackCommons.swift in Sources */,
1929B8CA73D3702364903BB7 /* SimpleCache.swift in Sources */,
1929BCA615324C58582BFC3C /* ProcessUtils.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -466,7 +470,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 282;
CURRENT_PROJECT_VERSION = 284;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@ -526,7 +530,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 282;
CURRENT_PROJECT_VERSION = 284;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@ -553,7 +557,7 @@
COMBINE_HIDPI_IMAGES = YES;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 282;
DYLIB_CURRENT_VERSION = 284;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../Carthage/Build/Mac";
FRAMEWORK_VERSION = A;
@ -575,7 +579,7 @@
COMBINE_HIDPI_IMAGES = YES;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 282;
DYLIB_CURRENT_VERSION = 284;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../Carthage/Build/Mac";
FRAMEWORK_VERSION = A;

View File

@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>0.24.0</string>
<string>SNAPSHOT-284</string>
<key>CFBundleVersion</key>
<string>282</string>
<string>284</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2017 Tae Won Ha. All rights reserved.</string>
<key>NSPrincipalClass</key>

View File

@ -3,9 +3,12 @@
void recurseDraw(
const unichar *chars,
CGGlyph *glyphs, CGPoint *positions, UniCharCount length,
CGGlyph *glyphs,
CGPoint *positions,
UniCharCount length,
CGContextRef context,
CTFontRef fontRef,
NSMutableArray *fontCache,
BOOL isComposing,
BOOL useLigatures
);

View File

@ -9,7 +9,7 @@
*/
/**
* Extracted from snapshot-146 of MacVim
* Extracted from 351faf929e4abe32ea4cc31078d1a625fc86a69f of MacVim, 2018-07-03
* https://github.com/macvim-dev/macvim
* See VIM.LICENSE
*/
@ -20,7 +20,9 @@
#import "MMCoreTextView.h"
static CTFontRef
// @formatter:off
static CTFontRef
lookupFont(NSMutableArray *fontCache, const unichar *chars, UniCharCount count,
CTFontRef currFontRef)
{
@ -61,7 +63,7 @@ attributedStringForString(NSString *string, const CTFontRef font,
// 2 - full ligatures including rare
// 1 - basic ligatures
// 0 - no ligatures
[NSNumber numberWithInteger:(useLigatures ? 1 : 0)],
[NSNumber numberWithBool:useLigatures],
kCTLigatureAttributeName,
nil
];
@ -72,7 +74,7 @@ attributedStringForString(NSString *string, const CTFontRef font,
static UniCharCount
fetchGlyphsAndAdvances(const CTLineRef line, CGGlyph *glyphs, CGSize *advances,
UniCharCount length)
CGPoint *positions, UniCharCount length)
{
NSArray *glyphRuns = (NSArray*)CTLineGetGlyphRuns(line);
@ -82,19 +84,23 @@ fetchGlyphsAndAdvances(const CTLineRef line, CGGlyph *glyphs, CGSize *advances,
CTRunRef run = (CTRunRef)item;
CFIndex count = CTRunGetGlyphCount(run);
if (count > 0 && count - offset > length)
count = length - offset;
if (count > 0) {
if (count > length - offset)
count = length - offset;
CFRange range = CFRangeMake(0, count);
CFRange range = CFRangeMake(0, count);
if (glyphs != NULL)
CTRunGetGlyphs(run, range, &glyphs[offset]);
if (advances != NULL)
CTRunGetAdvances(run, range, &advances[offset]);
if (glyphs != NULL)
CTRunGetGlyphs(run, range, &glyphs[offset]);
if (advances != NULL)
CTRunGetAdvances(run, range, &advances[offset]);
if (positions != NULL)
CTRunGetPositions(run, range, &positions[offset]);
offset += count;
if (offset >= length)
break;
offset += count;
if (offset >= length)
break;
}
}
return offset;
@ -117,78 +123,28 @@ gatherGlyphs(CGGlyph glyphs[], UniCharCount count)
}
static UniCharCount
ligatureGlyphsForChars(const unichar *chars, CGGlyph *glyphs,
CGPoint *positions, UniCharCount length, CTFontRef font)
composeGlyphsForChars(const unichar *chars, CGGlyph *glyphs,
CGPoint *positions, UniCharCount length, CTFontRef font,
BOOL isComposing, BOOL useLigatures)
{
// CoreText has no simple wait of retrieving a ligature for a set of
// UniChars. The way proposed on the CoreText ML is to convert the text to
// an attributed string, create a CTLine from it and retrieve the Glyphs
// from the CTRuns in it.
CGGlyph refGlyphs[length];
CGPoint refPositions[length];
memcpy(refGlyphs, glyphs, sizeof(CGGlyph) * length);
memcpy(refPositions, positions, sizeof(CGSize) * length);
memset(glyphs, 0, sizeof(CGGlyph) * length);
NSString *plainText = [NSString stringWithCharacters:chars length:length];
CFAttributedStringRef ligatureText = attributedStringForString(plainText,
font, YES);
CFAttributedStringRef composedText = attributedStringForString(plainText,
font,
useLigatures);
CTLineRef ligature = CTLineCreateWithAttributedString(ligatureText);
CTLineRef line = CTLineCreateWithAttributedString(composedText);
CGSize ligatureRanges[length], regularRanges[length];
// get the (composing)glyphs and advances for the new text
UniCharCount offset = fetchGlyphsAndAdvances(line, glyphs, NULL,
isComposing ? positions : NULL,
length);
// get the (ligature)glyphs and advances for the new text
UniCharCount offset = fetchGlyphsAndAdvances(ligature, glyphs,
ligatureRanges, length);
// fetch the advances for the base text
CTFontGetAdvancesForGlyphs(font, kCTFontOrientationDefault, refGlyphs,
regularRanges, length);
CFRelease(composedText);
CFRelease(line);
CFRelease(ligatureText);
CFRelease(ligature);
// tricky part: compare both advance ranges and chomp positions which are
// covered by a single ligature while keeping glyphs not in the ligature
// font.
#define fequal(a, b) (fabs((a) - (b)) < FLT_EPSILON)
#define fless(a, b)((a) - (b) < FLT_EPSILON) && (fabs((a) - (b)) > FLT_EPSILON)
CFIndex skip = 0;
CFIndex i;
for (i = 0; i < offset && skip + i < length; ++i) {
memcpy(&positions[i], &refPositions[skip + i], sizeof(CGSize));
if (fequal(ligatureRanges[i].width, regularRanges[skip + i].width)) {
// [mostly] same width
continue;
} else if (fless(ligatureRanges[i].width,
regularRanges[skip + i].width)) {
// original is wider than our result - use the original glyph
// FIXME: this is currently the only way to detect emoji (except
// for 'glyph[i] == 5')
glyphs[i] = refGlyphs[skip + i];
continue;
}
// no, that's a ligature
// count how many positions this glyph would take up in the base text
CFIndex j = 0;
float width = ceil(regularRanges[skip + i].width);
while ((int)width < (int)ligatureRanges[i].width
&& skip + i + j < length) {
width += ceil(regularRanges[++j + skip + i].width);
}
skip += j;
}
#undef fless
#undef fequal
// as ligatures combine characters it is required to adjust the
// as ligatures composing characters it is required to adjust the
// original length value
return offset;
}
@ -196,18 +152,12 @@ ligatureGlyphsForChars(const unichar *chars, CGGlyph *glyphs,
void
recurseDraw(const unichar *chars, CGGlyph *glyphs, CGPoint *positions,
UniCharCount length, CGContextRef context, CTFontRef fontRef,
NSMutableArray *fontCache, BOOL useLigatures)
NSMutableArray *fontCache, BOOL isComposing, BOOL useLigatures)
{
if (CTFontGetGlyphsForCharacters(fontRef, chars, glyphs, length)) {
// All chars were mapped to glyphs, so draw all at once and return.
if (useLigatures) {
length = ligatureGlyphsForChars(chars, glyphs, positions, length,
fontRef);
} else {
// only fixup surrogate pairs if we're not using ligatures
length = gatherGlyphs(glyphs, length);
}
length = composeGlyphsForChars(chars, glyphs, positions, length,
fontRef, isComposing, useLigatures);
CTFontDrawGlyphs(fontRef, glyphs, positions, length, context);
return;
}
@ -268,7 +218,7 @@ recurseDraw(const unichar *chars, CGGlyph *glyphs, CGPoint *positions,
return;
recurseDraw(chars, glyphs, positions, attemptedCount, context,
fallback, fontCache, useLigatures);
fallback, fontCache, isComposing, useLigatures);
// If only a portion of the invalid range was rendered above,
// the remaining range needs to be attempted by subsequent
@ -292,4 +242,6 @@ recurseDraw(const unichar *chars, CGGlyph *glyphs, CGPoint *positions,
}
}
// @formatter:on
#pragma clang diagnostic pop

View File

@ -29,13 +29,13 @@ extension Api {
if checkBlocked {
return self
.checkBlocked(
self.rpc(method: "nvim_buf_get_info", params: params, expectsReturnValue: true)
self.rpc(method: "nvim_buf_get_info", params: params)
)
.map(transform)
}
return self
.rpc(method: "nvim_buf_get_info", params: params, expectsReturnValue: true)
.rpc(method: "nvim_buf_get_info", params: params)
.map(transform)
}
}

View File

@ -58,7 +58,7 @@ extension NvimView {
public func newTab() -> Completable {
return self.api
.command(command: "tabe", expectsReturnValue: false)
.command(command: "tabe")
.subscribeOn(self.scheduler)
}
@ -74,7 +74,7 @@ extension NvimView {
let bufExists = buffers.contains { $0.url == url }
let wins = tabs.map({ $0.windows }).flatMap({ $0 })
if let win = bufExists ? wins.first(where: { win in win.buffer.url == url }) : nil {
return self.api.setCurrentWin(window: Api.Window(win.handle), expectsReturnValue: false)
return self.api.setCurrentWin(window: Api.Window(win.handle))
}
return currentBufferIsTransient ? self.open(url, cmd: "e") : self.open(url, cmd: "tabe")
@ -112,43 +112,47 @@ extension NvimView {
.map { tabs in tabs.map { $0.windows }.flatMap { $0 } }
.flatMapCompletable { wins -> Completable in
if let win = wins.first(where: { $0.buffer == buffer }) {
return self.api.setCurrentWin(window: Api.Window(win.handle), expectsReturnValue: false)
return self.api.setCurrentWin(window: Api.Window(win.handle))
}
return self.api.command(command: "tab sb \(buffer.handle)", expectsReturnValue: false)
return self.api.command(command: "tab sb \(buffer.handle)")
}
.subscribeOn(self.scheduler)
}
public func goTo(line: Int) -> Completable {
return self.api.command(command: "\(line)")
}
/// Closes the current window.
public func closeCurrentTab() -> Completable {
return self.api
.command(command: "q", expectsReturnValue: true)
.command(command: "q")
.subscribeOn(self.scheduler)
}
public func saveCurrentTab() -> Completable {
return self.api
.command(command: "w", expectsReturnValue: true)
.command(command: "w")
.subscribeOn(self.scheduler)
}
public func saveCurrentTab(url: URL) -> Completable {
return self.api
.command(command: "w \(url.path)", expectsReturnValue: true)
.command(command: "w \(url.path)")
.subscribeOn(self.scheduler)
}
public func closeCurrentTabWithoutSaving() -> Completable {
return self.api
.command(command: "q!", expectsReturnValue: true)
.command(command: "q!")
.subscribeOn(self.scheduler)
}
public func quitNeoVimWithoutSaving() -> Completable {
self.bridgeLogger.mark()
return self.api
.command(command: "qa!", expectsReturnValue: true)
.command(command: "qa!")
.subscribeOn(self.scheduler)
}
@ -200,7 +204,7 @@ extension NvimView {
private func `open`(_ url: URL, cmd: String) -> Completable {
return self.api
.command(command: "\(cmd) \(url.path)", expectsReturnValue: false)
.command(command: "\(cmd) \(url.path)")
.subscribeOn(self.scheduler)
}

View File

@ -127,7 +127,7 @@ extension NvimView: NSTouchBarDelegate, NSScrubberDataSource, NSScrubberDelegate
let window = tab.currentWindow ?? tab.windows[0]
self.api
.setCurrentWin(window: Api.Window(window.handle), expectsReturnValue: false)
.setCurrentWin(window: Api.Window(window.handle))
.subscribeOn(self.scheduler)
.subscribe(onError: { error in
self.eventsSubject.onNext(.apiError(msg: "Could not set current window to \(window.handle).", cause: error))

View File

@ -17,14 +17,17 @@ public class NvimView: NSView,
var useInteractiveZsh: Bool
var cwd: URL
var nvimArgs: [String]?
var envDict: [String: String]?
public init(useInteractiveZsh: Bool,
cwd: URL = URL(fileURLWithPath: NSHomeDirectory()),
nvimArgs: [String]? = nil) {
cwd: URL,
nvimArgs: [String]?,
envDict: [String: String]?) {
self.useInteractiveZsh = useInteractiveZsh
self.cwd = cwd
self.nvimArgs = nvimArgs
self.envDict = envDict
}
}
@ -171,7 +174,7 @@ public class NvimView: NSView,
set {
self.api
.setCurrentDir(dir: newValue.path, expectsReturnValue: false)
.setCurrentDir(dir: newValue.path)
.subscribeOn(self.scheduler)
.subscribe(onError: { error in
self.eventsSubject.onError(Error.ipc(msg: "Could not set cwd to \(newValue)", cause: error))
@ -310,7 +313,10 @@ public class NvimView: NSView,
}
convenience override public init(frame rect: NSRect) {
self.init(frame: rect, config: Config(useInteractiveZsh: false))
self.init(frame: rect, config: Config(useInteractiveZsh: false,
cwd: URL(fileURLWithPath: NSHomeDirectory()),
nvimArgs: nil,
envDict: nil))
}
required public init?(coder: NSCoder) {

View File

@ -0,0 +1,51 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
class ProcessUtils {
static func envVars(of shellPath: URL, usingInteractiveMode: Bool) -> [String: String] {
let shellName = shellPath.lastPathComponent
var shellArgs = [String]()
if shellName != "tcsh" {
shellArgs.append("-l")
}
if usingInteractiveMode {
shellArgs.append("-i")
}
shellArgs.append(contentsOf: ["-c", "env"])
let outputPipe = Pipe()
let errorPipe = Pipe()
let process = Process()
process.launchPath = shellPath.path
process.arguments = shellArgs
process.standardOutput = outputPipe
process.standardError = errorPipe
process.currentDirectoryPath = NSHomeDirectory()
process.launch()
let readHandle = outputPipe.fileHandleForReading
guard let output = String(data: readHandle.readDataToEndOfFile(), encoding: .utf8) else {
return [:]
}
readHandle.closeFile()
process.terminate()
process.waitUntilExit()
return output
.split(separator: "\n")
.reduce(into: [:]) { result, entry in
let split = entry.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false).map { String($0) }
result[split[0]] = split[1]
}
}
}

View File

@ -1,7 +1,8 @@
//
// Created by Tae Won Ha on 21.05.18.
// Copyright (c) 2018 Tae Won Ha. All rights reserved.
//
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation

View File

@ -192,7 +192,7 @@ static CGColorRef color_for(NSInteger value) {
const UniChar *bEnd = unichars + unilength;
UniCharCount choppedLength;
bool wide;
bool pWide = NO;
bool pWide = false;
while (b < bEnd) {
wide = CFStringIsSurrogateHighCharacter(*b) || CFStringIsSurrogateLowCharacter(*b);
@ -200,7 +200,8 @@ static CGColorRef color_for(NSInteger value) {
choppedLength = b - bStart;
// NSString *logged = [NSString stringWithCharacters:bStart length:choppedLength];
// NSLog(@"C(%d,%p..%p)[%@]", pWide, bStart, b, logged);
recurseDraw(bStart, glyphs, p, choppedLength, context, fontWithTraits, _fontLookupCache, _usesLigatures);
// We use isComposing = false to retain the old behavior of Macvim's recurseDraw
recurseDraw(bStart, glyphs, p, choppedLength, context, fontWithTraits, _fontLookupCache, false, _usesLigatures);
UniCharCount step = pWide ? choppedLength / 2 : choppedLength;
p += step;
g += step;
@ -214,7 +215,8 @@ static CGColorRef color_for(NSInteger value) {
choppedLength = b - bStart;
// NSString *logged = [NSString stringWithCharacters:bStart length:choppedLength];
// NSLog(@"T(%d,%p..%p)[%@]", pWide, bStart, b, logged);
recurseDraw(bStart, glyphs, p, choppedLength, context, fontWithTraits, _fontLookupCache, _usesLigatures);
// We use isComposing = false to retain the old behavior of Macvim's recurseDraw
recurseDraw(bStart, glyphs, p, choppedLength, context, fontWithTraits, _fontLookupCache, false, _usesLigatures);
}
// NSLog(@"S(-,%p..%p)[%@]", unichars, unichars + unilength, string);

View File

@ -64,6 +64,17 @@ class UiBridge {
self.nvimArgs = config.nvimArgs ?? []
self.cwd = config.cwd
if let envDict = config.envDict {
self.envDict = envDict
logger.debug("using envs from vimr: \(envDict)")
} else {
let selfEnv = ProcessInfo.processInfo.environment
let shellUrl = URL(fileURLWithPath: selfEnv["SHELL"] ?? "/bin/bash")
let interactiveMode = shellUrl.lastPathComponent == "zsh" && !config.useInteractiveZsh ? false : true
self.envDict = ProcessUtils.envVars(of: shellUrl, usingInteractiveMode: interactiveMode)
logger.debug("using envs from login shell: \(self.envDict)")
}
self.queue = queue
self.scheduler = SerialDispatchQueueScheduler(queue: queue,
internalSerialQueueName: String(reflecting: UiBridge.self))
@ -71,13 +82,13 @@ class UiBridge {
self.server.queue = self.queue
self.server.stream
.subscribe(onNext: { message in
self.handleMessage(msgId: message.msgid, data: message.data)
}, onError: { error in
self.logger.error("There was an error on the local message port server: \(error)")
self.streamSubject.onError(Error.ipc(error))
})
.disposed(by: self.disposeBag)
.subscribe(onNext: { message in
self.handleMessage(msgId: message.msgid, data: message.data)
}, onError: { error in
self.logger.error("There was an error on the local message port server: \(error)")
self.streamSubject.onError(Error.ipc(error))
})
.disposed(by: self.disposeBag)
}
func runLocalServerAndNvim(width: Int, height: Int) -> Completable {
@ -85,15 +96,15 @@ class UiBridge {
self.initialHeight = height
return self.server
.run(as: self.localServerName)
.andThen(Completable.create { completable in
self.runLocalServerAndNvimCompletable = completable
self.launchNvimUsingLoginShell()
.run(as: self.localServerName)
.andThen(Completable.create { completable in
self.runLocalServerAndNvimCompletable = completable
self.launchNvimUsingLoginShellEnv()
// This will be completed in .nvimReady branch of handleMessage()
return Disposables.create()
})
.timeout(timeout, scheduler: self.scheduler)
// This will be completed in .nvimReady branch of handleMessage()
return Disposables.create()
})
.timeout(timeout, scheduler: self.scheduler)
}
func vimInput(_ str: String) -> Completable {
@ -150,11 +161,11 @@ class UiBridge {
case .serverReady:
self
.establishNvimConnection()
.subscribe(onError: { error in
self.streamSubject.onError(Error.ipc(error))
})
.disposed(by: self.disposeBag)
.establishNvimConnection()
.subscribe(onError: { error in
self.streamSubject.onError(Error.ipc(error))
})
.disposed(by: self.disposeBag)
case .nvimReady:
self.runLocalServerAndNvimCompletable?(.completed)
@ -310,7 +321,7 @@ class UiBridge {
let dict = (try? unpack(d))?.value.dictionaryValue,
let key = dict.keys.first?.stringValue,
let value = dict.values.first
else {
else {
return
}
@ -331,31 +342,31 @@ class UiBridge {
private func closePorts() -> Completable {
return self.client
.stop()
.andThen(self.server.stop())
.stop()
.andThen(self.server.stop())
}
private func quit(using body: @escaping () -> Void) -> Completable {
return self
.closePorts()
.andThen(Completable.create { completable in
body()
.closePorts()
.andThen(Completable.create { completable in
body()
completable(.completed)
return Disposables.create()
})
completable(.completed)
return Disposables.create()
})
}
private func establishNvimConnection() -> Completable {
return self.client
.connect(to: self.remoteServerName)
.andThen(self.sendMessage(msgId: .agentReady, data: [self.initialWidth, self.initialHeight].data()))
.connect(to: self.remoteServerName)
.andThen(self.sendMessage(msgId: .agentReady, data: [self.initialWidth, self.initialHeight].data()))
}
private func sendMessage(msgId: NvimBridgeMsgId, data: Data?) -> Completable {
return self.client
.send(msgid: Int32(msgId.rawValue), data: data, expectsReply: false)
.asCompletable()
.send(msgid: Int32(msgId.rawValue), data: data, expectsReply: false)
.asCompletable()
}
private func forceExitNvimServer() {
@ -363,47 +374,23 @@ class UiBridge {
self.nvimServerProc?.terminate()
}
private func launchNvimUsingLoginShell() {
let selfEnv = ProcessInfo.processInfo.environment
let shellPath = URL(fileURLWithPath: selfEnv["SHELL"] ?? "/bin/bash")
let shellName = shellPath.lastPathComponent
var shellArgs = [String]()
if shellName != "tcsh" {
// tcsh does not like the -l option
shellArgs.append("-l")
}
if self.useInteractiveZsh && shellName == "zsh" {
shellArgs.append("-i")
}
private func launchNvimUsingLoginShellEnv() {
let listenAddress = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("vimr_\(self.uuid).sock")
var env = selfEnv
var env = self.envDict
env["NVIM_LISTEN_ADDRESS"] = listenAddress.path
let inputPipe = Pipe()
let outPipe = Pipe()
let errorPipe = Pipe()
let process = Process()
process.environment = env
process.standardInput = inputPipe
process.standardError = errorPipe
process.standardOutput = outPipe
process.currentDirectoryPath = self.cwd.path
process.launchPath = shellPath.path
process.arguments = shellArgs
process.launchPath = self.nvimServerExecutablePath()
process.arguments = [self.localServerName, self.remoteServerName] + ["--headless"] + self.nvimArgs
process.launch()
self.nvimServerProc = process
nvimArgs.append("--headless")
let cmd = "exec '\(self.nvimServerExecutablePath())' '\(self.localServerName)' '\(self.remoteServerName)' "
.appending(self.nvimArgs.map { "'\($0)'" }.joined(separator: " "))
self.logger.debug(cmd)
let writeHandle = inputPipe.fileHandleForWriting
guard let cmdData = cmd.data(using: .utf8) else {
preconditionFailure("Could not get Data from the string '\(cmd)'")
}
writeHandle.write(cmdData)
writeHandle.closeFile()
}
private func nvimServerExecutablePath() -> String {
@ -420,7 +407,8 @@ class UiBridge {
private let useInteractiveZsh: Bool
private let cwd: URL
private var nvimArgs: [String]
private let nvimArgs: [String]
private let envDict: [String: String]
private let server = RxMessagePortServer()
private let client = RxMessagePortClient()

View File

@ -12,7 +12,8 @@
1929B05B9D664052EC2D23EF /* FileOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BCE3E156C06EDF1F2806 /* FileOutlineView.swift */; };
1929B08C6230B9C5AB72DAF1 /* Pref128ToCurrentConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B5046239709E33516F5C /* Pref128ToCurrentConverter.swift */; };
1929B0E0C3BC59F52713D5A2 /* FoundationCommons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B9AF20D7BD6E5C975128 /* FoundationCommons.swift */; };
1929B0F599D1F62C7BE53D2C /* HttpServerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B1DC584C89C477E83FA2 /* HttpServerService.swift */; };
1929B0F599D1F62C7BE53D2C /* HttpServerMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B1DC584C89C477E83FA2 /* HttpServerMiddleware.swift */; };
1929B10EE7FE8DC251B741B2 /* RxRedux.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B66A5E2D00EA143AFD86 /* RxRedux.swift */; };
1929B1837C750CADB3A5BCB9 /* OpenQuicklyFileViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B1558455B3A74D93EF2A /* OpenQuicklyFileViewRow.swift */; };
1929B20CE35B43BB1CE023BA /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BC2F05E9A5C0DB039739 /* Theme.swift */; };
1929B29B95AD176D57942E08 /* UiRootReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B457B9D0FA4D21F3751E /* UiRootReducer.swift */; };
@ -36,7 +37,7 @@
1929B5543B1E31A26096E656 /* FileMonitorReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B04EC69F616EEFAF5F96 /* FileMonitorReducer.swift */; };
1929B59FA5C286E010F70BEE /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BFC0A5A9C6DB09BE1368 /* Types.swift */; };
1929B5A2EE366F79ED32744C /* KeysPrefReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B88B5FA08E897A3C2168 /* KeysPrefReducer.swift */; };
1929B5C1BABBC0D09D97C3EF /* PreviewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B617C229B19DB3E987B8 /* PreviewService.swift */; };
1929B5C1BABBC0D09D97C3EF /* PreviewMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B617C229B19DB3E987B8 /* PreviewMiddleware.swift */; };
1929B5F016431A76292D1E84 /* FileMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B365A6434354B568B04F /* FileMonitor.swift */; };
1929B6388EAF16C190B82955 /* FileItemIgnorePattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B69499B2569793350CEC /* FileItemIgnorePattern.swift */; };
1929B6460862447A31B5B082 /* ImageAndTextTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BDC3F82CB4CB4FE56D1B /* ImageAndTextTableCell.swift */; };
@ -54,13 +55,14 @@
1929B9318D32146D58BB38EC /* AppKitCommons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6A70931D60E04200E12030 /* AppKitCommons.swift */; };
1929B94083273D4B321AD848 /* FileItemUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B56C8ED31834BA9D8543 /* FileItemUtils.swift */; };
1929B98F94536E3912AD9F3B /* ArrayCommonsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BAF13FAD5DA8D3762367 /* ArrayCommonsTest.swift */; };
1929B990A143763A56CFCED0 /* PrefService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B364460D86F17E80943C /* PrefService.swift */; };
1929B990A143763A56CFCED0 /* PrefMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B364460D86F17E80943C /* PrefMiddleware.swift */; };
1929BA715337FE26155B2071 /* BufferList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BA43449BA41666CD55ED /* BufferList.swift */; };
1929BA76A1D97D8226F7CFB1 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B6AD3396160AA2C46919 /* Debouncer.swift */; };
1929BAAD7336FDFF1F78E749 /* ScorerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BF69B01107F358CF7EAD /* ScorerTest.swift */; };
1929BAE4900D72A7877741B1 /* PrefWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BE168F31344B69E61B62 /* PrefWindow.swift */; };
1929BAFF1E011321D3186EE6 /* UiRoot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BD4149D5A25C82064DD8 /* UiRoot.swift */; };
1929BB4A9B2FA42A64CCCC76 /* MainWindowReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BD83A13BF133741766CC /* MainWindowReducer.swift */; };
1929BB67CAAD4F6CBD38DF0A /* RxRedux.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B66A5E2D00EA143AFD86 /* RxRedux.swift */; };
1929BBE28654E4307AF1E2FD /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BC2F05E9A5C0DB039739 /* Theme.swift */; };
1929BCC7908DD899999B70BE /* AppearancePrefReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BED01F5D94BFCA4CF80F /* AppearancePrefReducer.swift */; };
1929BCC9D3604933DFF07E2E /* FileBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BA5C7099CDEB04B76BA4 /* FileBrowser.swift */; };
@ -278,16 +280,17 @@
1929B12CE56A9B36980288A4 /* OpenQuicklyReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenQuicklyReducer.swift; sourceTree = "<group>"; };
1929B14A5949FB64C4B2646F /* KeysPref.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeysPref.swift; sourceTree = "<group>"; };
1929B1558455B3A74D93EF2A /* OpenQuicklyFileViewRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenQuicklyFileViewRow.swift; sourceTree = "<group>"; };
1929B1DC584C89C477E83FA2 /* HttpServerService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpServerService.swift; sourceTree = "<group>"; };
1929B1DC584C89C477E83FA2 /* HttpServerMiddleware.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpServerMiddleware.swift; sourceTree = "<group>"; };
1929B34FC23D805A8B29E8F7 /* Context.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Context.swift; sourceTree = "<group>"; };
1929B364460D86F17E80943C /* PrefService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefService.swift; sourceTree = "<group>"; };
1929B364460D86F17E80943C /* PrefMiddleware.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefMiddleware.swift; sourceTree = "<group>"; };
1929B365A6434354B568B04F /* FileMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileMonitor.swift; sourceTree = "<group>"; };
1929B457B9D0FA4D21F3751E /* UiRootReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UiRootReducer.swift; sourceTree = "<group>"; };
1929B49E6924847AD085C8C9 /* PrefWindowReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefWindowReducer.swift; sourceTree = "<group>"; };
1929B5046239709E33516F5C /* Pref128ToCurrentConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pref128ToCurrentConverter.swift; sourceTree = "<group>"; };
1929B56C8ED31834BA9D8543 /* FileItemUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileItemUtils.swift; sourceTree = "<group>"; };
1929B5D45C9792BBE76B8AFF /* StringCommonsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringCommonsTest.swift; sourceTree = "<group>"; };
1929B617C229B19DB3E987B8 /* PreviewService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewService.swift; sourceTree = "<group>"; };
1929B617C229B19DB3E987B8 /* PreviewMiddleware.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewMiddleware.swift; sourceTree = "<group>"; };
1929B66A5E2D00EA143AFD86 /* RxRedux.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RxRedux.swift; sourceTree = "<group>"; };
1929B67A10E6BB2986B2416E /* BufferListReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BufferListReducer.swift; sourceTree = "<group>"; };
1929B694508FB5FDE607513A /* ToolsPrefReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToolsPrefReducer.swift; sourceTree = "<group>"; };
1929B69499B2569793350CEC /* FileItemIgnorePattern.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileItemIgnorePattern.swift; sourceTree = "<group>"; };
@ -565,7 +568,7 @@
1929B34FC23D805A8B29E8F7 /* Context.swift */,
1929B32401E8914DE9BF76CA /* Components */,
1929B5E773BDB3B4EE9D00C1 /* Reducers */,
1929BFA93DC859DD76C46192 /* Services */,
1929BFA93DC859DD76C46192 /* Middlewares */,
1929BFC0A5A9C6DB09BE1368 /* Types.swift */,
1929BA42AB6F1BF631B57399 /* SerializableStates.swift */,
);
@ -621,14 +624,14 @@
name = "Open Quickly";
sourceTree = "<group>";
};
1929BFA93DC859DD76C46192 /* Services */ = {
1929BFA93DC859DD76C46192 /* Middlewares */ = {
isa = PBXGroup;
children = (
1929B617C229B19DB3E987B8 /* PreviewService.swift */,
1929B1DC584C89C477E83FA2 /* HttpServerService.swift */,
1929B364460D86F17E80943C /* PrefService.swift */,
1929B617C229B19DB3E987B8 /* PreviewMiddleware.swift */,
1929B1DC584C89C477E83FA2 /* HttpServerMiddleware.swift */,
1929B364460D86F17E80943C /* PrefMiddleware.swift */,
);
name = Services;
name = Middlewares;
sourceTree = "<group>";
};
4B5012001EBA791000F76C46 /* Frameworks */ = {
@ -807,6 +810,7 @@
4B6423941D8EFD6100FC78C8 /* Workspace */,
4B97E2CF1D33F92200FC0660 /* resources */,
1929BA652D3B88FC071531EC /* UI */,
1929B66A5E2D00EA143AFD86 /* RxRedux.swift */,
);
path = VimR;
sourceTree = "<group>";
@ -1059,8 +1063,8 @@
4BE45C1D1FD2DBD2005C0A95 /* Logger.swift in Sources */,
1929B8FB248D71BF88A35761 /* PreviewTool.swift in Sources */,
1929B4B70926DE113E6BF990 /* PreviewReducer.swift in Sources */,
1929B5C1BABBC0D09D97C3EF /* PreviewService.swift in Sources */,
1929B0F599D1F62C7BE53D2C /* HttpServerService.swift in Sources */,
1929B5C1BABBC0D09D97C3EF /* PreviewMiddleware.swift in Sources */,
1929B0F599D1F62C7BE53D2C /* HttpServerMiddleware.swift in Sources */,
1929B3AC66EFE35D68C020E3 /* PreviewToolReducer.swift in Sources */,
1929B59FA5C286E010F70BEE /* Types.swift in Sources */,
1929B6D8F5FC723B7109031F /* OpenQuicklyReducer.swift in Sources */,
@ -1083,7 +1087,7 @@
1929B3557317755A43513B17 /* OpenQuicklyWindow.swift in Sources */,
1929B333855A5406C400DA92 /* OpenQuicklyFilterOperation.swift in Sources */,
1929B1837C750CADB3A5BCB9 /* OpenQuicklyFileViewRow.swift in Sources */,
1929B990A143763A56CFCED0 /* PrefService.swift in Sources */,
1929B990A143763A56CFCED0 /* PrefMiddleware.swift in Sources */,
1929BA76A1D97D8226F7CFB1 /* Debouncer.swift in Sources */,
1929B71610FF1DC6E459BA49 /* PreviewUtils.swift in Sources */,
1929B08C6230B9C5AB72DAF1 /* Pref128ToCurrentConverter.swift in Sources */,
@ -1103,6 +1107,7 @@
1929BE0F64A6CE5BCE2A5092 /* MainWindow+Delegates.swift in Sources */,
1929B8F498D1E7C53F572CE2 /* KeysPref.swift in Sources */,
1929B5A2EE366F79ED32744C /* KeysPrefReducer.swift in Sources */,
1929BB67CAAD4F6CBD38DF0A /* RxRedux.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1130,6 +1135,7 @@
1929B8E90A1378E494D481E7 /* PrefUtilsTest.swift in Sources */,
1929B20CE35B43BB1CE023BA /* Theme.swift in Sources */,
1929B9318D32146D58BB38EC /* AppKitCommons.swift in Sources */,
1929B10EE7FE8DC251B741B2 /* RxRedux.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1224,7 +1230,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 282;
CURRENT_PROJECT_VERSION = 284;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@ -1281,7 +1287,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 282;
CURRENT_PROJECT_VERSION = 284;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;

View File

@ -5,11 +5,12 @@
import Foundation
class AdvancedPrefReducer {
class AdvancedPrefReducer: ReducerType {
typealias Pair = StateActionPair<AppState, AdvancedPref.Action>
typealias StateType = AppState
typealias ActionType = AdvancedPref.Action
func reduce(_ pair: Pair) -> Pair {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
switch pair.action {
@ -29,6 +30,6 @@ class AdvancedPrefReducer {
state.useSnapshotUpdate = value
}
return StateActionPair(state: state, action: pair.action)
return (state, pair.action, true)
}
}

View File

@ -12,10 +12,21 @@ import CocoaFontAwesome
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {
struct OpenConfig {
var urls: [URL]
var cwd: URL
var cliPipePath: String?
var nvimArgs: [String]?
var envDict: [String: String]?
var line: Int?
}
enum Action {
case newMainWindow(urls: [URL], cwd: URL, nvimArgs: [String]?, cliPipePath: String?)
case openInKeyWindow(urls: [URL], cwd: URL, cliPipePath: String?)
case newMainWindow(config: OpenConfig)
case openInKeyWindow(config: OpenConfig)
case preferences
}
@ -24,10 +35,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
let baseServerUrl = URL(string: "http://localhost:\(NetUtils.openPort())")!
var initialAppState: AppState
if let stateDict = UserDefaults.standard.value(forKey: PrefService.compatibleVersion) as? [String: Any] {
if let stateDict = UserDefaults.standard.value(forKey: PrefMiddleware.compatibleVersion) as? [String: Any] {
initialAppState = AppState(dict: stateDict) ?? .default
} else {
if let oldDict = UserDefaults.standard.value(forKey: PrefService.lastCompatibleVersion) as? [String: Any] {
if let oldDict = UserDefaults.standard.value(forKey: PrefMiddleware.lastCompatibleVersion) as? [String: Any] {
initialAppState = Pref128ToCurrentConverter.appState(from: oldDict)
} else {
initialAppState = .default
@ -37,15 +48,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
baseServerUrl.appendingPathComponent(HtmlPreviewToolReducer.selectFirstPath)
)
self.stateContext = Context(baseServerUrl: baseServerUrl, state: initialAppState)
self.emit = self.stateContext.actionEmitter.typedEmit()
self.context = Context(baseServerUrl: baseServerUrl, state: initialAppState)
self.emit = self.context.actionEmitter.typedEmit()
self.openNewMainWindowOnLaunch = initialAppState.openNewMainWindowOnLaunch
self.openNewMainWindowOnReactivation = initialAppState.openNewMainWindowOnReactivation
self.useSnapshot = initialAppState.useSnapshotUpdate
let source = self.stateContext.stateSource
self.uiRoot = UiRoot(source: source, emitter: self.stateContext.actionEmitter, state: initialAppState)
let source = self.context.stateSource
self.uiRoot = UiRoot(source: source, emitter: self.context.actionEmitter, state: initialAppState)
super.init()
@ -90,7 +101,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
}
}
private let stateContext: Context
private let context: Context
private let emit: (Action) -> Void
private let uiRoot: UiRoot
@ -157,7 +168,7 @@ extension AppDelegate {
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
self.stateContext.savePrefs()
self.context.savePrefs()
if self.hasDirtyWindows && self.hasMainWindows {
let alert = NSAlert()
@ -188,7 +199,10 @@ extension AppDelegate {
// For drag & dropping files on the App icon.
func application(_ sender: NSApplication, openFiles filenames: [String]) {
let urls = filenames.map { URL(fileURLWithPath: $0) }
self.emit(.newMainWindow(urls: urls, cwd: FileUtils.userHomeUrl, nvimArgs: nil, cliPipePath: nil))
let config = OpenConfig(
urls: urls, cwd: FileUtils.userHomeUrl, cliPipePath: nil, nvimArgs: nil, envDict: nil, line: nil
)
self.emit(.newMainWindow(config: config))
sender.reply(toOpenOrPrint: .success)
}
@ -242,6 +256,21 @@ extension AppDelegate {
return
}
let envDict: [String: String]?
if let envPath = queryParam(envPathPrefix, from: rawParams, transforming: identity).first {
envDict = stringDict(from: URL(fileURLWithPath: envPath))
if FileManager.default.fileExists(atPath: envPath) {
do {
try FileManager.default.removeItem(atPath: envPath)
} catch {
fileLog.error(error.localizedDescription)
}
}
} else {
envDict = nil
}
let line = queryParam(linePrefix, from: rawParams, transforming: { Int($0) }).compactMap { $0 }.first
let urls = queryParam(filePrefix, from: rawParams, transforming: { URL(fileURLWithPath: $0) })
let cwd = queryParam(cwdPrefix,
from: rawParams,
@ -258,26 +287,48 @@ extension AppDelegate {
switch action {
case .activate, .newWindow:
self.emit(.newMainWindow(urls: urls, cwd: cwd, nvimArgs: nil, cliPipePath: pipePath))
let config = OpenConfig(urls: urls, cwd: cwd, cliPipePath: pipePath, nvimArgs: nil, envDict: envDict, line: line)
self.emit(.newMainWindow(config: config))
case .open:
self.emit(.openInKeyWindow(urls: urls, cwd: cwd, cliPipePath: pipePath))
let config = OpenConfig(urls: urls, cwd: cwd, cliPipePath: pipePath, nvimArgs: nil, envDict: envDict, line: line)
self.emit(.openInKeyWindow(config: config))
case .separateWindows:
urls.forEach { self.emit(.newMainWindow(urls: [$0], cwd: cwd, nvimArgs: nil, cliPipePath: pipePath)) }
urls.forEach {
let config = OpenConfig(urls: [$0], cwd: cwd, cliPipePath: pipePath, nvimArgs: nil, envDict: nil, line: line)
self.emit(.newMainWindow(config: config))
}
case .nvim:
self.emit(.newMainWindow(urls: [],
cwd: cwd,
nvimArgs: queryParam(nvimArgsPrefix, from: rawParams, transforming: identity),
cliPipePath: pipePath))
let config = OpenConfig(urls: urls,
cwd: cwd,
cliPipePath: pipePath,
nvimArgs: queryParam(nvimArgsPrefix, from: rawParams, transforming: identity),
envDict: envDict,
line: line)
self.emit(.newMainWindow(config: config))
}
}
private func stringDict(from jsonUrl: URL) -> [String: String]? {
guard let data = try? Data(contentsOf: jsonUrl) else {
return nil
}
do {
return try JSONSerialization.jsonObject(with: data) as? [String: String]
} catch {
fileLog.error(error.localizedDescription)
}
return nil
}
private func queryParam<T>(_ prefix: String,
from rawParams: [String],
transforming transform: (String) -> T) -> [T] {
from rawParams: [String],
transforming transform: (String) -> T) -> [T] {
return rawParams
.filter { $0.hasPrefix(prefix) }
@ -294,7 +345,10 @@ extension AppDelegate {
}
@IBAction func newDocument(_ sender: Any?) {
self.emit(.newMainWindow(urls: [], cwd: FileUtils.userHomeUrl, nvimArgs: nil, cliPipePath: nil))
let config = OpenConfig(
urls: [], cwd: FileUtils.userHomeUrl, cliPipePath: nil, nvimArgs: nil, envDict: nil, line: nil
)
self.emit(.newMainWindow(config: config))
}
@IBAction func openInNewWindow(_ sender: Any?) {
@ -318,7 +372,10 @@ extension AppDelegate {
let urls = panel.urls
let commonParentUrl = FileUtils.commonParent(of: urls)
self.emit(.newMainWindow(urls: urls, cwd: commonParentUrl, nvimArgs: nil, cliPipePath: nil))
let config = OpenConfig(
urls: urls, cwd: commonParentUrl, cliPipePath: nil, nvimArgs: nil, envDict: nil, line: nil
)
self.emit(.newMainWindow(config: config))
}
}
}
@ -331,7 +388,7 @@ extension AppDelegate {
}
}
/// Keep the rawValues in sync with Action in the `vimr` Python script.
// Keep the rawValues in sync with Action in the `vimr` Python script.
private enum VimRUrlAction: String {
case activate = "activate"
case open = "open"
@ -344,8 +401,11 @@ private let updater = SUUpdater()
private let debugMenuItemIdentifier = NSUserInterfaceItemIdentifier("debug-menu-item")
// Keep in sync with QueryParamKey in the `vimr` Python script.
private let filePrefix = "file="
private let cwdPrefix = "cwd="
private let nvimArgsPrefix = "nvim-args="
private let pipePathPrefix = "pipe-path="
private let waitPrefix = "wait="
private let envPathPrefix = "env-path="
private let linePrefix = "line="

View File

@ -5,59 +5,53 @@
import Foundation
class AppDelegateReducer {
class AppDelegateReducer: ReducerType {
typealias Pair = StateActionPair<AppState, AppDelegate.Action>
typealias StateType = AppState
typealias ActionType = AppDelegate.Action
init(baseServerUrl: URL) {
self.baseServerUrl = baseServerUrl
}
func reduce(_ pair: Pair) -> Pair {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
switch pair.action {
case let .newMainWindow(urls, cwd, nvimArgs, cliPipePath):
let mainWindow: MainWindow.State
if let args = nvimArgs {
mainWindow = self.newMainWindow(with: state, urls: [], cwd: cwd, nvimArgs: args, cliPipePath: cliPipePath)
} else {
mainWindow = self.newMainWindow(with: state, urls: urls, cwd: cwd, cliPipePath: cliPipePath)
}
case let .newMainWindow(config):
let mainWindow = self.newMainWindow(with: state, config: config)
state.mainWindows[mainWindow.uuid] = mainWindow
case let .openInKeyWindow(urls, cwd, cliPipePath):
case let .openInKeyWindow(config):
guard let uuid = state.currentMainWindowUuid, state.mainWindows[uuid] != nil else {
let mainWindow = self.newMainWindow(with: state, urls: urls, cwd: cwd, cliPipePath: cliPipePath)
let mainWindow = self.newMainWindow(with: state, config: config)
state.mainWindows[mainWindow.uuid] = mainWindow
break
}
state.mainWindows[uuid]?.urlsToOpen = urls.toDict { url in MainWindow.OpenMode.default }
state.mainWindows[uuid]?.cwd = cwd
state.mainWindows[uuid]?.urlsToOpen = config.urls.toDict { url in MainWindow.OpenMode.default }
state.mainWindows[uuid]?.cwd = config.cwd
if let line = config.line {
state.mainWindows[uuid]?.goToLineFromCli = Marked(line)
}
case .preferences:
state.preferencesOpen = Marked(true)
}
return StateActionPair(state: state, action: pair.action)
return (state, pair.action, true)
}
private let baseServerUrl: URL
private func newMainWindow(with state: AppState,
urls: [URL],
cwd: URL,
nvimArgs: [String]? = nil,
cliPipePath: String? = nil) -> MainWindow.State {
private func newMainWindow(with state: AppState, config: AppDelegate.OpenConfig) -> MainWindow.State {
var mainWindow = state.mainWindowTemplate
mainWindow.uuid = UUID().uuidString
mainWindow.cwd = cwd
mainWindow.cwd = config.cwd
mainWindow.isDirty = false
mainWindow.htmlPreview = HtmlPreviewState(
@ -66,11 +60,15 @@ class AppDelegateReducer {
)
mainWindow.preview.server = self.baseServerUrl.appendingPathComponent(MarkdownReducer.nonePath)
mainWindow.nvimArgs = nvimArgs
mainWindow.cliPipePath = cliPipePath
mainWindow.urlsToOpen = urls.toDict { _ in MainWindow.OpenMode.default }
mainWindow.nvimArgs = config.nvimArgs
mainWindow.cliPipePath = config.cliPipePath
mainWindow.envDict = config.envDict
mainWindow.urlsToOpen = config.urls.toDict { _ in MainWindow.OpenMode.default }
mainWindow.frame = state.mainWindows.isEmpty ? state.mainWindowTemplate.frame
: self.frame(relativeTo: state.mainWindowTemplate.frame)
: self.frame(relativeTo: state.mainWindowTemplate.frame)
if let line = config.line {
mainWindow.goToLineFromCli = Marked(line)
}
return mainWindow
}

View File

@ -5,11 +5,12 @@
import Foundation
class AppearancePrefReducer {
class AppearancePrefReducer: ReducerType {
typealias Pair = StateActionPair<AppState, AppearancePref.Action>
typealias StateType = AppState
typealias ActionType = AppearancePref.Action
func reduce(_ pair: Pair) -> Pair {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
var appearance = state.mainWindowTemplate.appearance
@ -34,7 +35,7 @@ class AppearancePrefReducer {
self.modify(state: &state, with: appearance)
return StateActionPair(state: state, action: pair.action)
return (state, pair.action, true)
}
private func modify(state: inout AppState, with appearance: AppearanceState) {

View File

@ -5,20 +5,21 @@
import Foundation
class BuffersListReducer {
class BuffersListReducer: ReducerType {
typealias Pair = StateActionPair<UuidState<MainWindow.State>, BuffersList.Action>
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<BuffersList.Action>
func reduce(_ pair: Pair) -> Pair {
var state = pair.state.payload
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch pair.action {
switch tuple.action.payload {
case let .open(buffer):
state.currentBufferToSet = buffer
}
return StateActionPair(state: UuidState(uuid: pair.state.uuid, state: state), action: pair.action)
return (state, tuple.action, true)
}
}

View File

@ -6,179 +6,120 @@
import Foundation
import RxSwift
class Context {
typealias AnyAction = Any
extension ReduxTypes {
let stateSource: Observable<AppState>
let actionEmitter = ActionEmitter()
typealias StateType = AppState
typealias ActionType = AnyAction
}
class Context: ReduxContext {
// The following should only be used when Cmd-Q'ing
func savePrefs() {
self.prefService.applyPref(from: self.appState)
self.prefMiddleware.applyPref(from: self.state)
}
init(baseServerUrl: URL, state: AppState) {
self.appState = state
self.stateSource = self.stateSubject.asObservable()
super.init(initialState: state)
let openQuicklyReducer = OpenQuicklyReducer()
let previewMiddleware = PreviewMiddleware()
let markdownReducer = MarkdownReducer(baseServerUrl: baseServerUrl)
let previewService = PreviewService()
let httpService: HttpServerService = HttpServerService(port: baseServerUrl.port!)
let httpMiddleware: HttpServerMiddleware = HttpServerMiddleware(port: baseServerUrl.port!)
let uiRootReducer = UiRootReducer()
let openQuicklyReducer = OpenQuicklyReducer()
// AppState
Observable
.of(
self.actionSourceForAppState()
.reduce(by: AppDelegateReducer(baseServerUrl: baseServerUrl).reduce)
.filterMapPair(),
self.actionSourceForAppState()
.reduce(by: uiRootReducer.reduceMainWindow)
.reduce(by: openQuicklyReducer.reduceMainWindow)
.filter { $0.modified }
.apply(self.prefService.applyMainWindow)
.map { $0.state },
self.actionSourceForAppState()
.reduce(by: FileMonitorReducer().reduce)
.filterMapPair(),
self.actionSourceForAppState()
.reduce(by: openQuicklyReducer.reduceOpenQuicklyWindow)
.filterMapPair(),
self.actionSourceForAppState()
.reduce(by: uiRootReducer.reduceUiRoot)
.filterMapPair()
)
.merge()
self.actionEmitter.observable
.map { (state: self.state, action: $0, modified: false) }
.reduce(
by: [
AppDelegateReducer(baseServerUrl: baseServerUrl).reduce,
uiRootReducer.mainWindow.reduce,
openQuicklyReducer.mainWindow.reduce,
FileMonitorReducer().reduce,
openQuicklyReducer.reduce,
uiRootReducer.reduce,
// Preferences
PrefWindowReducer().reduce,
GeneralPrefReducer().reduce,
ToolsPrefReducer().reduce,
AppearancePrefReducer().reduce,
AdvancedPrefReducer().reduce,
KeysPrefReducer().reduce,
],
middlewares: [
self.prefMiddleware.mainWindow.apply,
self.prefMiddleware.apply,
])
.filter { $0.modified }
.subscribe(onNext: self.emitAppState)
.disposed(by: self.disposeBag)
// MainWindow.State
Observable
.of(
self.actionSourceForMainWindow()
.reduce(by: MainWindowReducer().reduce)
.reduce(by: markdownReducer.reduceMainWindow)
.filter { $0.modified }
.apply(previewService.applyMainWindow)
.apply(httpService.applyMainWindow)
.map { $0.state },
self.actionSourceForMainWindow()
.reduce(by: markdownReducer.reducePreviewTool)
.reduce(by: PreviewToolReducer(baseServerUrl: baseServerUrl).reduce)
.filter { $0.modified }
.apply(previewService.applyPreviewTool)
.map { $0.state },
self.actionSourceForMainWindow()
.reduce(by: HtmlPreviewToolReducer(baseServerUrl: baseServerUrl).reduce)
.filter { $0.modified }
.apply(httpService.applyHtmlPreview)
.map { $0.state },
self.actionSourceForMainWindow()
.reduce(by: FileBrowserReducer().reduce)
.filterMapPair(),
self.actionSourceForMainWindow()
.reduce(by: BuffersListReducer().reduce)
.reduce(by: markdownReducer.reduceBufferList)
.filter { $0.modified }
.apply(previewService.applyBufferList)
.map { $0.state }
self.actionEmitter.observable
.mapOmittingNil { action in
guard let uuidAction = action as? UuidTagged else {
return nil
}
guard let mainWindowState = self.state.mainWindows[uuidAction.uuid] else {
return nil
}
return (mainWindowState, action, false)
}
.reduce(
by: [
MainWindowReducer().reduce,
markdownReducer.mainWindow.reduce,
markdownReducer.previewTool.reduce,
PreviewToolReducer(baseServerUrl: baseServerUrl).reduce,
HtmlPreviewToolReducer(baseServerUrl: baseServerUrl).reduce,
FileBrowserReducer().reduce,
BuffersListReducer().reduce,
markdownReducer.buffersList.reduce,
],
middlewares: [
previewMiddleware.mainWindow.apply,
httpMiddleware.mainWindow.apply,
previewMiddleware.previewTool.apply,
httpMiddleware.htmlPreview.apply,
previewMiddleware.buffersList.apply,
]
)
.merge()
.filter { $0.modified }
.subscribe(onNext: self.emitAppState)
.disposed(by: self.disposeBag)
// Preferences
Observable
.of(
self.prefStateSource(by: PrefWindowReducer().reduce, prefService: prefService),
self.prefStateSource(by: GeneralPrefReducer().reduce, prefService: prefService),
self.prefStateSource(by: ToolsPrefReducer().reduce, prefService: prefService),
self.prefStateSource(by: AppearancePrefReducer().reduce, prefService: prefService),
self.prefStateSource(by: AdvancedPrefReducer().reduce, prefService: prefService),
self.prefStateSource(by: KeysPrefReducer().reduce, prefService: prefService)
)
.merge()
.subscribe(onNext: self.emitAppState)
.disposed(by: self.disposeBag)
#if DEBUG
// self.actionEmitter.observable.debug().subscribe().disposed(by: self.disposeBag)
// stateSource.debug().subscribe().disposed(by: self.disposeBag)
#endif
}
deinit {
self.stateSubject.onCompleted()
}
private let prefMiddleware = PrefMiddleware()
private let stateSubject = PublishSubject<AppState>()
private let scheduler = SerialDispatchQueueScheduler(qos: .userInitiated)
private let disposeBag = DisposeBag()
private func emitAppState(_ tuple: (state: MainWindow.State, action: AnyAction, modified: Bool)) {
guard let uuidAction = tuple.action as? UuidTagged else {
return
}
private var appState: AppState
private let prefService = PrefService()
private func emitAppState(_ mainWindow: UuidState<MainWindow.State>) {
self.appState.mainWindows[mainWindow.uuid] = mainWindow.payload
self.stateSubject.onNext(self.appState)
self.state.mainWindows[uuidAction.uuid] = tuple.state
self.stateSubject.onNext(self.state)
self.cleanUpAppState()
}
private func emitAppState(_ appState: AppState) {
self.appState = appState
self.stateSubject.onNext(self.appState)
private func emitAppState(_ tuple: ReduxTypes.ReduceTuple) {
self.state = tuple.state
self.stateSubject.onNext(self.state)
self.cleanUpAppState()
}
private func cleanUpAppState() {
self.appState.mainWindows.keys.forEach { uuid in
self.appState.mainWindows[uuid]?.cwdToSet = nil
self.appState.mainWindows[uuid]?.currentBufferToSet = nil
self.appState.mainWindows[uuid]?.viewToBeFocused = nil
self.appState.mainWindows[uuid]?.urlsToOpen.removeAll()
self.state.mainWindows.keys.forEach { uuid in
self.state.mainWindows[uuid]?.cwdToSet = nil
self.state.mainWindows[uuid]?.currentBufferToSet = nil
self.state.mainWindows[uuid]?.viewToBeFocused = nil
self.state.mainWindows[uuid]?.urlsToOpen.removeAll()
}
}
private func actionSourceForAppState<ActionType>() -> Observable<StateActionPair<AppState, ActionType>> {
return self.actionEmitter.observable
.mapOmittingNil { $0 as? ActionType }
.map { self.appStateActionPair(for: $0) }
}
private func actionSourceForMainWindow<ActionType>()
-> Observable<StateActionPair<UuidState<MainWindow.State>, ActionType>> {
return self.actionEmitter.observable
.mapOmittingNil { $0 as? UuidAction<ActionType> }
.mapOmittingNil { self.mainWindowStateActionPair(for: $0) }
}
private func prefStateSource<ActionType>(
by reduce: @escaping (StateActionPair<AppState, ActionType>) -> StateActionPair<AppState, ActionType>,
prefService: PrefService
) -> Observable<AppState> {
return self.actionSourceForAppState()
.reduce(by: reduce)
.filter { $0.modified }
.apply(self.prefService.applyPref)
.map { $0.state }
}
private func appStateActionPair<ActionType>(for action: ActionType) -> StateActionPair<AppState, ActionType> {
return StateActionPair(state: self.appState, action: action, modified: false)
}
private func mainWindowStateActionPair<ActionType>(for action: UuidAction<ActionType>)
-> StateActionPair<UuidState<MainWindow.State>, ActionType>? {
guard let mainWindowState = self.appState.mainWindows[action.uuid] else {
return nil
}
return StateActionPair(state: UuidState(uuid: action.uuid, state: mainWindowState),
action: action.payload,
modified: false)
}
}

View File

@ -5,14 +5,15 @@
import Foundation
class FileBrowserReducer {
class FileBrowserReducer: ReducerType {
typealias Pair = StateActionPair<UuidState<MainWindow.State>, FileBrowser.Action>
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<FileBrowser.Action>
func reduce(_ pair: Pair) -> Pair {
var state = pair.state.payload
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch pair.action {
switch tuple.action.payload {
case let .open(url, mode):
state.urlsToOpen[url] = mode
@ -29,6 +30,6 @@ class FileBrowserReducer {
}
return StateActionPair(state: UuidState(uuid: state.uuid, state: state), action: pair.action)
return (state, tuple.action, true)
}
}

View File

@ -5,14 +5,15 @@
import Foundation
class FileMonitorReducer {
class FileMonitorReducer: ReducerType {
typealias Pair = StateActionPair<AppState, FileMonitor.Action>
typealias StateType = AppState
typealias ActionType = FileMonitor.Action
func reduce(_ pair: Pair) -> Pair {
var state = pair.state
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch pair.action {
switch tuple.action {
case let .change(in: url):
if let fileItem = FileItemUtils.item(for: url, root: state.openQuickly.root, create: false) {
@ -28,6 +29,6 @@ class FileMonitorReducer {
}
return StateActionPair(state: state, action: pair.action)
return (state, tuple.action, true)
}
}

View File

@ -5,11 +5,12 @@
import Foundation
class GeneralPrefReducer {
class GeneralPrefReducer: ReducerType {
typealias Pair = StateActionPair<AppState, GeneralPref.Action>
typealias StateType = AppState
typealias ActionType = GeneralPref.Action
func reduce(_ pair: Pair) -> Pair {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
switch pair.action {
@ -29,6 +30,6 @@ class GeneralPrefReducer {
}
return StateActionPair(state: state, action: pair.action)
return (state, pair.action, true)
}
}

View File

@ -5,9 +5,10 @@
import Foundation
class HtmlPreviewToolReducer {
class HtmlPreviewToolReducer: ReducerType {
typealias Pair = StateActionPair<UuidState<MainWindow.State>, HtmlPreviewTool.Action>
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<HtmlPreviewTool.Action>
static let basePath = "/tools/html-preview"
static let selectFirstPath = "/tools/html-preview/select-first.html"
@ -16,11 +17,11 @@ class HtmlPreviewToolReducer {
self.baseServerUrl = baseServerUrl
}
func reduce(_ pair: Pair) -> Pair {
var state = pair.state.payload
let uuid = pair.state.uuid
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
let uuid = pair.action.uuid
switch pair.action {
switch pair.action.payload {
case let .selectHtmlFile(url):
state.htmlPreview.htmlFile = url
@ -30,7 +31,7 @@ class HtmlPreviewToolReducer {
}
return StateActionPair(state: UuidState(uuid: state.uuid, state: state), action: pair.action)
return (state, pair.action, true)
}
private let baseServerUrl: URL

View File

@ -0,0 +1,121 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
import Swifter
class HttpServerMiddleware {
let htmlPreview: HtmlPreviewMiddleware
let mainWindow: MainWindowMiddleware
init(port: Int) {
let server = HttpServer()
let resourceUrl = Bundle.main.resourceURL!
let githubCssUrl = resourceUrl.appendingPathComponent("markdown/github-markdown.css")
self.htmlPreview = HtmlPreviewMiddleware(server: server, githubCssUrl: githubCssUrl)
self.mainWindow = MainWindowMiddleware(server: server, githubCssUrl: githubCssUrl)
do {
try server.start(in_port_t(port))
stdoutLog.info("VimR http server started on http://localhost:\(port)")
let previewResourceUrl = resourceUrl.appendingPathComponent("preview")
server["\(MarkdownReducer.basePath)/:path"] = shareFilesFromDirectory(previewResourceUrl.path)
server.GET["\(MarkdownReducer.basePath)/github-markdown.css"] = shareFile(githubCssUrl.path)
server["\(HtmlPreviewToolReducer.basePath)/:path"] = shareFilesFromDirectory(previewResourceUrl.path)
server.GET["\(HtmlPreviewToolReducer.basePath)/github-markdown.css"] = shareFile(githubCssUrl.path)
} catch {
stdoutLog.error("Server could not be started on port \(port): \(error)")
}
}
class HtmlPreviewMiddleware: MiddlewareType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<HtmlPreviewTool.Action>
init(server: HttpServer, githubCssUrl: URL) {
self.server = server
self.githubCssUrl = githubCssUrl
}
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction {
return { tuple in
let result = reduce(tuple)
guard tuple.modified else {
return result
}
let state = result.state
guard let serverUrl = state.htmlPreview.server, let htmlFileUrl = state.htmlPreview.htmlFile else {
return result
}
let basePath = serverUrl.payload.deletingLastPathComponent().path
self.server.GET[serverUrl.payload.path] = shareFile(htmlFileUrl.path)
self.server["\(basePath)/:path"] = shareFilesFromDirectory(htmlFileUrl.parent.path)
return result
}
}
private let server: HttpServer
private let githubCssUrl: URL
}
class MainWindowMiddleware: MiddlewareType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<MainWindow.Action>
init(server: HttpServer, githubCssUrl: URL) {
self.server = server
self.githubCssUrl = githubCssUrl
}
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction {
return { tuple in
let result = reduce(tuple)
guard tuple.modified else {
return result
}
let uuidAction = tuple.action
guard case .newCurrentBuffer = uuidAction.payload else {
return result
}
let preview = result.state.preview
guard case .markdown = preview.status,
let buffer = preview.buffer,
let html = preview.html,
let server = preview.server else {
return result
}
fileLog.debug("Serving \(html) on \(server)")
let htmlBasePath = server.deletingLastPathComponent().path
self.server["\(htmlBasePath)/:path"] = shareFilesFromDirectory(buffer.deletingLastPathComponent().path)
self.server.GET[server.path] = shareFile(html.path)
self.server.GET["\(htmlBasePath)/github-markdown.css"] = shareFile(self.githubCssUrl.path)
return result
}
}
private let server: HttpServer
private let githubCssUrl: URL
}
}

View File

@ -1,72 +0,0 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
import Swifter
class HttpServerService {
typealias HtmlPreviewPair = StateActionPair<UuidState<MainWindow.State>, HtmlPreviewTool.Action>
typealias MainWindowPair = StateActionPair<UuidState<MainWindow.State>, MainWindow.Action>
init(port: Int) {
let resourceUrl = Bundle.main.resourceURL!
self.githubCssUrl = resourceUrl.appendingPathComponent("markdown/github-markdown.css")
do {
try self.server.start(in_port_t(port))
stdoutLog.info("VimR http server started on http://localhost:\(port)")
let previewResourceUrl = resourceUrl.appendingPathComponent("preview")
self.server["\(MarkdownReducer.basePath)/:path"] = shareFilesFromDirectory(previewResourceUrl.path)
self.server.GET["\(MarkdownReducer.basePath)/github-markdown.css"] = shareFile(githubCssUrl.path)
self.server["\(HtmlPreviewToolReducer.basePath)/:path"] = shareFilesFromDirectory(previewResourceUrl.path)
self.server.GET["\(HtmlPreviewToolReducer.basePath)/github-markdown.css"] = shareFile(githubCssUrl.path)
} catch {
NSLog("ERROR server could not be started on port \(port)")
}
}
func applyHtmlPreview(_ pair: HtmlPreviewPair) {
let state = pair.state.payload
guard let serverUrl = state.htmlPreview.server, let htmlFileUrl = state.htmlPreview.htmlFile else {
return
}
let basePath = serverUrl.payload.deletingLastPathComponent().path
self.server.GET[serverUrl.payload.path] = shareFile(htmlFileUrl.path)
self.server["\(basePath)/:path"] = shareFilesFromDirectory(htmlFileUrl.parent.path)
}
func applyMainWindow(_ pair: MainWindowPair) {
guard case .newCurrentBuffer = pair.action else {
return
}
let preview = pair.state.payload.preview
guard case .markdown = preview.status,
let buffer = preview.buffer,
let html = preview.html,
let server = preview.server
else {
return
}
fileLog.debug("Serving \(html) on \(server)")
let htmlBasePath = server.deletingLastPathComponent().path
self.server["\(htmlBasePath)/:path"] = shareFilesFromDirectory(buffer.deletingLastPathComponent().path)
self.server.GET[server.path] = shareFile(html.path)
self.server.GET["\(htmlBasePath)/github-markdown.css"] = shareFile(self.githubCssUrl.path)
}
private let server = HttpServer()
private let githubCssUrl: URL
}

View File

@ -1224,7 +1224,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.24.0</string>
<string>SNAPSHOT-284</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@ -1241,7 +1241,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>282</string>
<string>284</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
<key>LSMinimumSystemVersion</key>

View File

@ -5,11 +5,12 @@
import Foundation
class KeysPrefReducer {
class KeysPrefReducer: ReducerType {
typealias Pair = StateActionPair<AppState, KeysPref.Action>
typealias StateType = AppState
typealias ActionType = KeysPref.Action
func reduce(_ pair: Pair) -> Pair {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
switch pair.action {
@ -24,6 +25,6 @@ class KeysPrefReducer {
}
return StateActionPair(state: state, action: pair.action)
return (state, pair.action, true)
}
}

View File

@ -112,7 +112,8 @@ class MainWindow: NSObject,
let neoVimViewConfig = NvimView.Config(useInteractiveZsh: state.useInteractiveZsh,
cwd: state.cwd,
nvimArgs: state.nvimArgs)
nvimArgs: state.nvimArgs,
envDict: state.envDict)
self.neoVimView = NvimView(frame: .zero, config: neoVimViewConfig)
self.neoVimView.configureForAutoLayout()
@ -259,23 +260,39 @@ class MainWindow: NSObject,
if let cwd = state.cwdToSet {
self.neoVimView.cwd = cwd
}
if state.preview.status == .markdown
&& state.previewTool.isReverseSearchAutomatically
&& state.preview.previewPosition.hasDifferentMark(as: self.previewPosition) {
self.neoVimView
.cursorGo(to: state.preview.previewPosition.payload)
.subscribe()
}
self.previewPosition = state.preview.previewPosition
Completable
.empty()
.andThen {
if state.preview.status == .markdown
&& state.previewTool.isReverseSearchAutomatically
&& state.preview.previewPosition.hasDifferentMark(as: self.previewPosition) {
self.open(urls: state.urlsToOpen)
self.previewPosition = state.preview.previewPosition
return self.neoVimView.cursorGo(to: state.preview.previewPosition.payload)
}
if let currentBuffer = state.currentBufferToSet {
self.neoVimView
.select(buffer: currentBuffer)
.subscribe()
}
return .empty()
}
.andThen(self.open(urls: state.urlsToOpen))
.andThen {
if let currentBuffer = state.currentBufferToSet {
return self.neoVimView.select(buffer: currentBuffer)
}
return .empty()
}
.andThen {
if self.goToLineFromCli?.mark != state.goToLineFromCli?.mark {
self.goToLineFromCli = state.goToLineFromCli
if let goToLine = self.goToLineFromCli {
return self.neoVimView.goTo(line: goToLine.payload)
}
}
return .empty()
}
.subscribe()
let usesTheme = state.appearance.usesTheme
let themePrefChanged = state.appearance.usesTheme != self.usesTheme
@ -335,7 +352,17 @@ class MainWindow: NSObject,
self.window.setFrame(state.frame, display: true)
self.window.makeFirstResponder(self.neoVimView)
self.open(urls: state.urlsToOpen)
self.goToLineFromCli = state.goToLineFromCli
self
.open(urls: state.urlsToOpen)
.andThen {
if let goToLine = self.goToLineFromCli {
return self.neoVimView.goTo(line: goToLine.payload)
}
return .empty()
}
.subscribe()
}
func uuidAction(for action: Action) -> UuidAction<Action> {
@ -364,6 +391,8 @@ class MainWindow: NSObject,
private var currentBuffer: NvimView.Buffer?
private var goToLineFromCli: Marked<Int>?
private var defaultFont = NvimView.defaultFont
private var linespacing = NvimView.defaultLinespacing
private var usesLigatures = false
@ -402,26 +431,25 @@ class MainWindow: NSObject,
self.workspace.theme = workspaceTheme
}
private func open(urls: [URL: OpenMode]) {
// If we don't call the following in the next tick, only half of the existing swap file warning is displayed.
// Dunno why...
DispatchQueue.main.async {
Completable.concat(
urls.map { entry -> Completable in
let url = entry.key
let mode = entry.value
switch mode {
case .default: return self.neoVimView.open(urls: [url])
case .currentTab: return self.neoVimView.openInCurrentTab(url: url)
case .newTab: return self.neoVimView.openInNewTab(urls: [url])
case .horizontalSplit: return self.neoVimView.openInHorizontalSplit(urls: [url])
case .verticalSplit: return self.neoVimView.openInVerticalSplit(urls: [url])
}
}
)
.subscribe()
private func open(urls: [URL: OpenMode]) -> Completable {
if urls.isEmpty {
return .empty()
}
return .concat(
urls.map { entry -> Completable in
let url = entry.key
let mode = entry.value
switch mode {
case .default: return self.neoVimView.open(urls: [url])
case .currentTab: return self.neoVimView.openInCurrentTab(url: url)
case .newTab: return self.neoVimView.openInNewTab(urls: [url])
case .horizontalSplit: return self.neoVimView.openInHorizontalSplit(urls: [url])
case .verticalSplit: return self.neoVimView.openInVerticalSplit(urls: [url])
}
}
)
}
private func addViews() {

View File

@ -5,14 +5,15 @@
import Foundation
class MainWindowReducer {
class MainWindowReducer: ReducerType {
typealias Pair = StateActionPair<UuidState<MainWindow.State>, MainWindow.Action>
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<MainWindow.Action>
func reduce(_ pair: Pair) -> Pair {
var state = pair.state.payload
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch pair.action {
switch tuple.action.payload {
case let .frameChanged(to:frame):
state.frame = frame
@ -30,8 +31,8 @@ class MainWindowReducer {
case let .setDirtyStatus(status):
// When I gt or w around, we change tab somehow... Dunno why...
if status == pair.state.payload.isDirty {
return pair
if status == tuple.state.isDirty {
return tuple
}
state.isDirty = status
@ -78,10 +79,10 @@ class MainWindowReducer {
state.appearance.theme = Marked(theme)
default:
return pair
return tuple
}
return StateActionPair(state: UuidState(uuid: state.uuid, state: state), action: pair.action)
return (state, tuple.action, true)
}
}

View File

@ -6,12 +6,14 @@
import Foundation
import RxSwift
class OpenQuicklyReducer {
class OpenQuicklyReducer: ReducerType {
typealias OpenQuicklyWindowPair = StateActionPair<AppState, OpenQuicklyWindow.Action>
typealias MainWindowPair = StateActionPair<AppState, UuidAction<MainWindow.Action>>
typealias StateType = AppState
typealias ActionType = OpenQuicklyWindow.Action
func reduceOpenQuicklyWindow(_ pair: OpenQuicklyWindowPair) -> OpenQuicklyWindowPair {
let mainWindow = MainWindowReducer()
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var appState = pair.state
appState.openQuickly.open = false
@ -32,31 +34,37 @@ class OpenQuicklyReducer {
}
return StateActionPair(state: appState, action: pair.action)
return (appState, pair.action, true)
}
func reduceMainWindow(_ pair: MainWindowPair) -> MainWindowPair {
switch pair.action.payload {
class MainWindowReducer: ReducerType {
case .openQuickly:
var appState = pair.state
typealias StateType = AppState
typealias ActionType = UuidAction<MainWindow.Action>
guard let uuid = appState.currentMainWindowUuid else {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
switch pair.action.payload {
case .openQuickly:
var appState = pair.state
guard let uuid = appState.currentMainWindowUuid else {
return pair
}
guard let cwd = appState.mainWindows[uuid]?.cwd else {
return pair
}
appState.openQuickly.open = true
appState.openQuickly.cwd = cwd
return (appState, pair.action, true)
default:
return pair
}
guard let cwd = appState.mainWindows[uuid]?.cwd else {
return pair
}
appState.openQuickly.open = true
appState.openQuickly.cwd = cwd
return StateActionPair(state: appState, action: pair.action)
default:
return pair
}
}
}

View File

@ -0,0 +1,62 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
class PrefMiddleware: MiddlewareType {
typealias StateType = AppState
typealias ActionType = AnyAction
static let compatibleVersion = "168"
static let lastCompatibleVersion = "128"
let mainWindow = MainWindowMiddleware()
// The following should only be used when Cmd-Q'ing
func applyPref(from appState: AppState) {
defaults.setValue(appState.dict(), forKey: PrefMiddleware.compatibleVersion)
}
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction {
return { tuple in
let result = reduce(tuple)
guard tuple.modified else {
return result
}
defaults.setValue(result.state.dict(), forKey: PrefMiddleware.compatibleVersion)
return result
}
}
class MainWindowMiddleware: MiddlewareType {
typealias StateType = AppState
typealias ActionType = UuidAction<MainWindow.Action>
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction {
return { tuple in
let result = reduce(tuple)
guard tuple.modified else {
return result
}
let uuidAction = tuple.action
guard case .close = uuidAction.payload else {
return result
}
defaults.setValue(result.state.dict(), forKey: PrefMiddleware.compatibleVersion)
return result
}
}
}
}
private let defaults = UserDefaults.standard

View File

@ -1,33 +0,0 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
private let defaults = UserDefaults.standard
class PrefService {
typealias MainWindowPair = StateActionPair<AppState, UuidAction<MainWindow.Action>>
static let compatibleVersion = "168"
static let lastCompatibleVersion = "128"
// The following should only be used when Cmd-Q'ing
func applyPref(from appState: AppState) {
defaults.setValue(appState.dict(), forKey: PrefService.compatibleVersion)
}
func applyPref<ActionType>(_ pair: StateActionPair<AppState, ActionType>) {
defaults.setValue(pair.state.dict(), forKey: PrefService.compatibleVersion)
}
func applyMainWindow(_ pair: MainWindowPair) {
guard case .close = pair.action.payload else {
return
}
defaults.setValue(pair.state.dict(), forKey: PrefService.compatibleVersion)
}
}

View File

@ -5,11 +5,12 @@
import Foundation
class PrefWindowReducer {
class PrefWindowReducer: ReducerType {
typealias Pair = StateActionPair<AppState, PrefWindow.Action>
typealias StateType = AppState
typealias ActionType = PrefWindow.Action
func reduce(_ pair: Pair) -> Pair {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
switch pair.action {
@ -19,6 +20,6 @@ class PrefWindowReducer {
}
return StateActionPair(state: state, action: pair.action)
return (state, pair.action, true)
}
}

View File

@ -0,0 +1,186 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
import CocoaMarkdown
class PreviewMiddleware {
let previewTool: PreviewToolMiddleware
let buffersList: BuffersListMiddleware
let mainWindow: MainWindowMiddleware
init() {
let generator = PreviewGenerator()
self.previewTool = PreviewToolMiddleware(generator: generator)
self.buffersList = BuffersListMiddleware(generator: generator)
self.mainWindow = MainWindowMiddleware(generator: generator)
}
class PreviewGenerator {
init() {
guard let templateUrl = Bundle.main.url(forResource: "template",
withExtension: "html",
subdirectory: "markdown")
else {
preconditionFailure("ERROR Cannot load markdown template")
}
guard let template = try? String(contentsOf: templateUrl) else {
preconditionFailure("ERROR Cannot load markdown template")
}
self.template = template
}
func apply(_ state: MainWindow.State, uuid: String) {
let preview = state.preview
guard let buffer = preview.buffer, let html = preview.html else {
guard let previewUrl = self.previewFiles[uuid] else {
return
}
try? FileManager.default.removeItem(at: previewUrl)
self.previewFiles.removeValue(forKey: uuid)
return
}
fileLog.debug("\(buffer) -> \(html)")
do {
try self.render(buffer, to: html)
self.previewFiles[uuid] = html
} catch let error as NSError {
// FIXME: error handling!
NSLog("ERROR rendering \(buffer) to \(html): \(error)")
return
}
}
private func render(_ bufferUrl: URL, to htmlUrl: URL) throws {
let doc = CMDocument(contentsOfFile: bufferUrl.path, options: .sourcepos)
let renderer = CMHTMLRenderer(document: doc)
guard let body = renderer?.render() else {
// FIXME: error handling!
return
}
let html = filledTemplate(body: body, title: bufferUrl.lastPathComponent)
let htmlFilePath = htmlUrl.path
try html.write(toFile: htmlFilePath, atomically: true, encoding: .utf8)
}
private func filledTemplate(body: String, title: String) -> String {
return self.template
.replacingOccurrences(of: "{{ title }}", with: title)
.replacingOccurrences(of: "{{ body }}", with: body)
}
private let template: String
private var previewFiles = [String: URL]()
}
class PreviewToolMiddleware: MiddlewareType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<PreviewTool.Action>
init(generator: PreviewGenerator) {
self.generator = generator
}
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction {
return { tuple in
let result = reduce(tuple)
guard tuple.modified else {
return result
}
let uuidAction = tuple.action
guard case .refreshNow = uuidAction.payload else {
return result
}
self.generator.apply(result.state, uuid: uuidAction.uuid)
return result
}
}
private let generator: PreviewGenerator
}
class BuffersListMiddleware: MiddlewareType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<BuffersList.Action>
init(generator: PreviewGenerator) {
self.generator = generator
}
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction {
return { tuple in
let result = reduce(tuple)
guard tuple.modified else {
return result
}
let uuidAction = tuple.action
guard case .open = uuidAction.payload else {
return result
}
self.generator.apply(result.state, uuid: uuidAction.uuid)
return result
}
}
private let generator: PreviewGenerator
}
class MainWindowMiddleware: MiddlewareType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<MainWindow.Action>
init(generator: PreviewGenerator) {
self.generator = generator
}
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction {
return { tuple in
let result = reduce(tuple)
guard tuple.modified else {
return result
}
let uuidAction = tuple.action
switch uuidAction.payload {
case .newCurrentBuffer:
self.generator.apply(result.state, uuid: uuidAction.uuid)
case .bufferWritten:
self.generator.apply(result.state, uuid: uuidAction.uuid)
default:
return result
}
return result
}
}
private let generator: PreviewGenerator
}
}

View File

@ -7,124 +7,160 @@ import Foundation
class MarkdownReducer {
typealias PreviewToolPair = StateActionPair<UuidState<MainWindow.State>, PreviewTool.Action>
typealias BufferListPair = StateActionPair<UuidState<MainWindow.State>, BuffersList.Action>
typealias MainWindowPair = StateActionPair<UuidState<MainWindow.State>, MainWindow.Action>
static let basePath = "/tools/markdown"
static let saveFirstPath = "/tools/markdown/save-first.html"
static let errorPath = "/tools/markdown/error.html"
static let nonePath = "/tools/markdown/empty.html"
func reducePreviewTool(_ pair: PreviewToolPair) -> PreviewToolPair {
var state = pair.state.payload
switch pair.action {
case .refreshNow:
state.preview = PreviewUtils.state(for: pair.state.uuid,
baseUrl: self.baseServerUrl,
buffer: state.currentBuffer,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .reload
case let .reverseSearch(to:position):
state.preview.previewPosition = Marked(position)
state.preview.lastSearch = .reverse
case let .scroll(to:position):
if state.preview.lastSearch == .reload {
state.preview.lastSearch = .none
break;
}
guard state.previewTool.isReverseSearchAutomatically && state.preview.lastSearch != .forward else {
state.preview.lastSearch = .none
state.preview.previewPosition = Marked(mark: state.preview.previewPosition.mark, payload: position)
break;
}
state.preview.previewPosition = Marked(position)
state.preview.lastSearch = .reverse
default:
return pair
}
return StateActionPair(state: UuidState(uuid: state.uuid, state: state), action: pair.action)
}
func reduceBufferList(_ pair: BufferListPair) -> BufferListPair {
var state = pair.state.payload
switch pair.action {
case let .open(buffer):
state.preview = PreviewUtils.state(for: pair.state.uuid,
baseUrl: self.baseServerUrl,
buffer: buffer,
editorPosition: Marked(.beginning),
previewPosition: Marked(.beginning))
state.preview.lastSearch = .none
}
return StateActionPair(state: UuidState(uuid: pair.state.uuid, state: state), action: pair.action)
}
func reduceMainWindow(_ pair: MainWindowPair) -> MainWindowPair {
var state = pair.state.payload
switch pair.action {
case let .newCurrentBuffer(buffer):
state.preview = PreviewUtils.state(for: pair.state.uuid, baseUrl: self.baseServerUrl, buffer: buffer,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .none
case .bufferWritten:
state.preview = PreviewUtils.state(for: pair.state.uuid,
baseUrl: self.baseServerUrl,
buffer: state.currentBuffer,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .reload
case let .setCursor(to:position):
if state.preview.lastSearch == .reload {
state.preview.lastSearch = .none
break
}
guard state.previewTool.isForwardSearchAutomatically && state.preview.lastSearch != .reverse else {
state.preview.editorPosition = Marked(mark: state.preview.editorPosition.mark, payload: position.payload)
state.preview.lastSearch = .none
break
}
state.preview.editorPosition = Marked(position.payload)
state.preview.lastSearch = .none // .none because the forward search does not invoke .scroll above.
case .close:
state.preview = PreviewUtils.state(for: .none,
baseUrl: self.baseServerUrl,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .none
default:
return pair
}
return StateActionPair(state: UuidState(uuid: pair.state.uuid, state: state), action: pair.action)
}
let previewTool: PreviewToolReducer
let buffersList: BuffersListReducer
let mainWindow: MainWindowReducer
init(baseServerUrl: URL) {
self.baseServerUrl = baseServerUrl
self.previewTool = PreviewToolReducer(baseServerUrl: baseServerUrl)
self.buffersList = BuffersListReducer(baseServerUrl: baseServerUrl)
self.mainWindow = MainWindowReducer(baseServerUrl: baseServerUrl)
}
private let baseServerUrl: URL
class PreviewToolReducer: ReducerType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<PreviewTool.Action>
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch tuple.action.payload {
case .refreshNow:
state.preview = PreviewUtils.state(for: tuple.state.uuid,
baseUrl: self.baseServerUrl,
buffer: state.currentBuffer,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .reload
case let .reverseSearch(to:position):
state.preview.previewPosition = Marked(position)
state.preview.lastSearch = .reverse
case let .scroll(to:position):
if state.preview.lastSearch == .reload {
state.preview.lastSearch = .none
break;
}
guard state.previewTool.isReverseSearchAutomatically && state.preview.lastSearch != .forward else {
state.preview.lastSearch = .none
state.preview.previewPosition = Marked(mark: state.preview.previewPosition.mark, payload: position)
break;
}
state.preview.previewPosition = Marked(position)
state.preview.lastSearch = .reverse
default:
return tuple
}
return (state, tuple.action, true)
}
init(baseServerUrl: URL) {
self.baseServerUrl = baseServerUrl
}
private let baseServerUrl: URL
}
class BuffersListReducer: ReducerType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<BuffersList.Action>
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch tuple.action.payload {
case let .open(buffer):
state.preview = PreviewUtils.state(for: tuple.state.uuid,
baseUrl: self.baseServerUrl,
buffer: buffer,
editorPosition: Marked(.beginning),
previewPosition: Marked(.beginning))
state.preview.lastSearch = .none
}
return (state, tuple.action, true)
}
init(baseServerUrl: URL) {
self.baseServerUrl = baseServerUrl
}
private let baseServerUrl: URL
}
class MainWindowReducer: ReducerType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<MainWindow.Action>
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch tuple.action.payload {
case let .newCurrentBuffer(buffer):
state.preview = PreviewUtils.state(for: tuple.state.uuid, baseUrl: self.baseServerUrl, buffer: buffer,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .none
case .bufferWritten:
state.preview = PreviewUtils.state(for: tuple.state.uuid,
baseUrl: self.baseServerUrl,
buffer: state.currentBuffer,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .reload
case let .setCursor(to:position):
if state.preview.lastSearch == .reload {
state.preview.lastSearch = .none
break
}
guard state.previewTool.isForwardSearchAutomatically && state.preview.lastSearch != .reverse else {
state.preview.editorPosition = Marked(mark: state.preview.editorPosition.mark, payload: position.payload)
state.preview.lastSearch = .none
break
}
state.preview.editorPosition = Marked(position.payload)
state.preview.lastSearch = .none // .none because the forward search does not invoke .scroll above.
case .close:
state.preview = PreviewUtils.state(for: .none,
baseUrl: self.baseServerUrl,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .none
default:
return tuple
}
return (state, tuple.action, true)
}
init(baseServerUrl: URL) {
self.baseServerUrl = baseServerUrl
}
private let baseServerUrl: URL
}
}

View File

@ -1,104 +0,0 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
import CocoaMarkdown
class PreviewService {
typealias PreviewToolPair = StateActionPair<UuidState<MainWindow.State>, PreviewTool.Action>
typealias BufferListPair = StateActionPair<UuidState<MainWindow.State>, BuffersList.Action>
typealias MainWindowPair = StateActionPair<UuidState<MainWindow.State>, MainWindow.Action>
init() {
guard let templateUrl = Bundle.main.url(forResource: "template",
withExtension: "html",
subdirectory: "markdown")
else {
preconditionFailure("ERROR Cannot load markdown template")
}
guard let template = try? String(contentsOf: templateUrl) else {
preconditionFailure("ERROR Cannot load markdown template")
}
self.template = template
}
func applyPreviewTool(_ pair: PreviewToolPair) {
guard case .refreshNow = pair.action else {
return
}
self.apply(pair.state)
}
func applyBufferList(_ pair: BufferListPair) {
guard case .open = pair.action else {
return
}
self.apply(pair.state)
}
func applyMainWindow(_ pair: MainWindowPair) {
switch pair.action {
case .newCurrentBuffer: self.apply(pair.state)
case .bufferWritten: self.apply(pair.state)
default: return
}
}
private func filledTemplate(body: String, title: String) -> String {
return self.template
.replacingOccurrences(of: "{{ title }}", with: title)
.replacingOccurrences(of: "{{ body }}", with: body)
}
private func render(_ bufferUrl: URL, to htmlUrl: URL) throws {
let doc = CMDocument(contentsOfFile: bufferUrl.path, options: .sourcepos)
let renderer = CMHTMLRenderer(document: doc)
guard let body = renderer?.render() else {
// FIXME: error handling!
return
}
let html = filledTemplate(body: body, title: bufferUrl.lastPathComponent)
let htmlFilePath = htmlUrl.path
try html.write(toFile: htmlFilePath, atomically: true, encoding: .utf8)
}
private func apply(_ state: UuidState<MainWindow.State>) {
let uuid = state.uuid
let preview = state.payload.preview
guard let buffer = preview.buffer, let html = preview.html else {
guard let previewUrl = self.previewFiles[uuid] else {
return
}
try? FileManager.default.removeItem(at: previewUrl)
self.previewFiles.removeValue(forKey: uuid)
return
}
// NSLog("\(buffer) -> \(html)")
do {
try self.render(buffer, to: html)
self.previewFiles[uuid] = html
} catch let error as NSError {
// FIXME: error handling!
NSLog("ERROR rendering \(buffer) to \(html): \(error)")
return
}
}
private let template: String
private let tempDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
private var previewFiles = [String: URL]()
}

View File

@ -5,18 +5,19 @@
import Foundation
class PreviewToolReducer {
class PreviewToolReducer: ReducerType {
typealias Pair = StateActionPair<UuidState<MainWindow.State>, PreviewTool.Action>
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<PreviewTool.Action>
init(baseServerUrl: URL) {
self.baseServerUrl = baseServerUrl
}
func reduce(_ pair: Pair) -> Pair {
var state = pair.state.payload
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch pair.action {
switch tuple.action.payload {
case let .setAutomaticReverseSearch(to:value):
state.previewTool.isReverseSearchAutomatically = value
@ -28,11 +29,11 @@ class PreviewToolReducer {
state.previewTool.isRefreshOnWrite = value
default:
return pair
return tuple
}
return StateActionPair(state: UuidState(uuid: state.uuid, state: state), action: pair.action)
return (state, tuple.action, true)
}
private let baseServerUrl: URL

215
VimR/VimR/RxRedux.swift Normal file
View File

@ -0,0 +1,215 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
import RxSwift
protocol ReduxContextType {
/**
Type that holds the global app state
- Important:
This type must be typealias'ed in `ReduxTypes` or in an extension thereof.
*/
associatedtype StateType
/**
"The greatest common divisor" for all actions used in the app: Assuming it is set to `ReduxTypes.ActionType` type,
the following must be true for any action
```
assert(someAction is ReduxTypes.ActionType) // which means
let actionWithMinimumType: ReduxTypes.ActionType = anyAction
```
Most probably this type will be set to `Any`.
- Important:
This type must be typealias'ed in `ReduxTypes` or in an extension thereof.
*/
associatedtype ActionType
typealias ReduceTuple = (state: StateType, action: ActionType, modified: Bool)
typealias ReduceFunction = (ReduceTuple) -> ReduceTuple
}
/**
`typealias` `StateType` and `ActionType` either within the class definition or in an extension.
*/
class ReduxTypes: ReduxContextType {
}
protocol ReducerType {
associatedtype StateType
associatedtype ActionType
typealias ReduceTuple = (state: StateType, action: ActionType, modified: Bool)
typealias ActionTypeErasedReduceTuple = (state: StateType, action: ReduxTypes.ActionType, modified: Bool)
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple
}
extension ReducerType {
func reduce(_ tuple: ActionTypeErasedReduceTuple) -> ActionTypeErasedReduceTuple {
guard let typedTuple = tuple as? ReduceTuple else {
return tuple
}
let typedResult = self.typedReduce(typedTuple)
return (state: typedResult.state, action: typedResult.action, modified: typedResult.modified)
}
}
protocol MiddlewareType {
associatedtype StateType
associatedtype ActionType
typealias ReduceTuple = (state: StateType, action: ActionType, modified: Bool)
typealias ActionTypeErasedReduceTuple = (state: StateType, action: ReduxTypes.ActionType, modified: Bool)
typealias TypedActionReduceFunction = (ReduceTuple) -> ActionTypeErasedReduceTuple
typealias ActionTypeErasedReduceFunction = (ActionTypeErasedReduceTuple) -> ActionTypeErasedReduceTuple
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction
}
extension MiddlewareType {
func apply(_ reduce: @escaping ActionTypeErasedReduceFunction) -> ActionTypeErasedReduceFunction {
return { tuple in
guard let typedTuple = tuple as? ReduceTuple else {
return reduce(tuple)
}
let typedReduce: (ReduceTuple) -> ActionTypeErasedReduceTuple = { typedTuple in
// We know that we can cast the typed action to ReduxTypes.ActionType
return reduce((state: typedTuple.state, action: typedTuple.action, modified: typedTuple.modified))
}
return self.typedApply(typedReduce)(typedTuple)
}
}
}
protocol UiComponent {
associatedtype StateType
init(source: Observable<StateType>, emitter: ActionEmitter, state: StateType)
}
class ActionEmitter {
let observable: Observable<ReduxTypes.ActionType>
init() {
self.observable = self.subject.asObservable().observeOn(scheduler)
}
func typedEmit<T>() -> ((T) -> Void) {
return { (action: T) in
self.subject.onNext(action)
}
}
func terminate() {
self.subject.onCompleted()
}
deinit {
self.subject.onCompleted()
}
private let scheduler = SerialDispatchQueueScheduler(qos: .userInteractive)
private let subject = PublishSubject<ReduxTypes.ActionType>()
}
class ReduxContext {
let actionEmitter = ActionEmitter()
let stateSource: Observable<ReduxTypes.StateType>
convenience init(initialState: ReduxTypes.StateType,
reducers: [ReduxTypes.ReduceFunction],
middlewares: [(@escaping ReduxTypes.ReduceFunction) -> ReduxTypes.ReduceFunction] = []) {
self.init(initialState: initialState)
self.actionEmitter.observable
.map { (state: self.state, action: $0, modified: false) }
.reduce(by: reducers, middlewares: middlewares)
.filter { $0.modified }
.subscribe(onNext: { tuple in
self.state = tuple.state
self.stateSubject.onNext(tuple.state)
})
.disposed(by: self.disposeBag)
}
init(initialState: ReduxTypes.StateType) {
self.state = initialState
self.stateSource = self.stateSubject.asObservable().observeOn(self.stateScheduler)
}
func terminate() {
self.actionEmitter.terminate()
self.stateSubject.onCompleted()
}
var state: ReduxTypes.StateType
let stateSubject = PublishSubject<ReduxTypes.StateType>()
let stateScheduler = SerialDispatchQueueScheduler(qos: .userInteractive)
let disposeBag = DisposeBag()
}
extension Observable {
func completableSubject() -> CompletableSubject<Element> {
return CompletableSubject(source: self)
}
}
class CompletableSubject<T> {
func asObservable() -> Observable<T> {
return self.subject.asObservable()
}
init(source: Observable<T>) {
let subject = PublishSubject<T>()
self.subscription = source.subscribe(onNext: { element in subject.onNext(element) })
self.subject = subject
}
func onCompleted() {
self.subject.onCompleted()
self.subscription.dispose()
}
private let subject: PublishSubject<T>
private let subscription: Disposable
}
extension Observable {
func reduce(
by reducers: [(Element) -> Element],
middlewares: [(@escaping (Element) -> Element) -> (Element) -> Element]
) -> Observable<Element> {
let dispatch = { pair in
return reducers.reduce(pair) { result, reduceBody in
return reduceBody(result)
}
}
let next = middlewares.reversed().reduce(dispatch) { result, middleware in middleware(result) }
return self.map(next)
}
}

View File

@ -6,7 +6,7 @@
import Cocoa
import RxSwift
extension ObservableType {
extension Observable {
func mapOmittingNil<R>(_ transform: @escaping (E) throws -> R?) -> RxSwift.Observable<R> {
return self
@ -16,6 +16,13 @@ extension ObservableType {
}
}
extension PrimitiveSequenceType where TraitType == CompletableTrait, ElementType == Never {
func andThen(using body: () -> Completable) -> Completable {
return self.andThen(body())
}
}
extension PrimitiveSequence where Element == Never, TraitType == CompletableTrait {
func wait() throws {
@ -58,10 +65,6 @@ extension PrimitiveSequence where TraitType == SingleTrait {
.ignoreElements()
}
func asCompletable() -> Completable {
return self.asObservable().ignoreElements()
}
func syncValue() -> Element? {
var trigger = false
var value: Element?

View File

@ -233,6 +233,7 @@ extension MainWindow {
var frame = CGRect(x: 100, y: 100, width: 600, height: 400)
////// transient
var goToLineFromCli: Marked<Int>?
var lastFileSystemUpdate = Marked(FileUtils.userHomeUrl)
var tools = WorkspaceToolState.default
@ -260,6 +261,7 @@ extension MainWindow {
var useInteractiveZsh = false
var nvimArgs: [String]?
var cliPipePath: String?
var envDict: [String: String]?
var isLeftOptionMeta = false
var isRightOptionMeta = false

View File

@ -5,11 +5,12 @@
import Foundation
class ToolsPrefReducer {
class ToolsPrefReducer: ReducerType {
typealias Pair = StateActionPair<AppState, ToolsPref.Action>
typealias StateType = AppState
typealias ActionType = ToolsPref.Action
func reduce(_ pair: Pair) -> Pair {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
switch pair.action {
@ -19,6 +20,6 @@ class ToolsPrefReducer {
}
return StateActionPair(state: state, action: pair.action)
return (state, pair.action, true)
}
}

View File

@ -6,49 +6,19 @@
import Foundation
import RxSwift
protocol UiComponent {
struct StateActionPair<S, A> {
associatedtype StateType
init(source: Observable<StateType>, emitter: ActionEmitter, state: StateType)
var state: S
var action: A
var modified: Bool
}
class ActionEmitter {
protocol UuidTagged {
let observable: Observable<Any>
init() {
self.observable = self.subject.asObservable().observeOn(scheduler)
}
func typedEmit<T>() -> ((T) -> Void) {
return { (action: T) in
self.subject.onNext(action)
}
}
deinit {
self.subject.onCompleted()
}
private let scheduler = SerialDispatchQueueScheduler(qos: .userInteractive)
private let subject = PublishSubject<Any>()
var uuid: String { get }
}
class StateActionPair<S, A> {
let modified: Bool
let state: S
let action: A
init(state: S, action: A, modified: Bool = true) {
self.modified = modified
self.state = state
self.action = action
}
}
class UuidAction<A>: CustomStringConvertible {
class UuidAction<A>: UuidTagged, CustomStringConvertible {
let uuid: String
let payload: A
@ -63,7 +33,7 @@ class UuidAction<A>: CustomStringConvertible {
}
}
class UuidState<S>: CustomStringConvertible {
class UuidState<S>: UuidTagged, CustomStringConvertible {
let uuid: String
let payload: S
@ -116,23 +86,6 @@ class Marked<T>: CustomStringConvertible {
}
}
extension Observable {
func reduce(by reduce: @escaping (Element) -> Element) -> Observable<Element> {
return self.map(reduce)
}
func apply(_ apply: @escaping (Element) -> Void) -> Observable<Element> {
return self.do(onNext: apply)
}
func filterMapPair<S, A>() -> Observable<S> where Element == StateActionPair<S, A> {
return self
.filter { $0.modified }
.map { $0.state }
}
}
class UiComponentTemplate: UiComponent {
typealias StateType = State

View File

@ -80,25 +80,19 @@ class UiRoot: UiComponent {
private let prefWindow: PrefWindow
private var mainWindows = [String: MainWindow]()
private var subjectForMainWindows = [String: PublishSubject<MainWindow.State>]()
private var disposables = [String: Disposable]()
private var subjectForMainWindows = [String: CompletableSubject<MainWindow.State>]()
private func newMainWindow(with state: MainWindow.State) -> MainWindow {
let subject = PublishSubject<MainWindow.State>()
let source = self.source.mapOmittingNil { $0.mainWindows[state.uuid] }
let subject = self.source.mapOmittingNil { $0.mainWindows[state.uuid] }.completableSubject()
self.subjectForMainWindows[state.uuid] = subject
self.disposables[state.uuid] = source.subscribe(subject)
return MainWindow(source: subject.asObservable(), emitter: self.emitter, state: state)
}
private func removeMainWindow(with uuid: String) {
self.subjectForMainWindows[uuid]?.onCompleted()
self.disposables[uuid]?.dispose()
self.subjectForMainWindows.removeValue(forKey: uuid)
self.disposables.removeValue(forKey: uuid)
self.mainWindows.removeValue(forKey: uuid)
}
}

View File

@ -5,92 +5,100 @@
import Foundation
class UiRootReducer {
class UiRootReducer: ReducerType {
typealias UiRootPair = StateActionPair<AppState, UiRoot.Action>
typealias MainWindowPair = StateActionPair<AppState, UuidAction<MainWindow.Action>>
typealias StateType = AppState
typealias ActionType = UiRoot.Action
func reduceUiRoot(_ pair: UiRootPair) -> UiRootPair {
var appState = pair.state
let mainWindow = MainWindowReducer()
switch pair.action {
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var appState = tuple.state
switch tuple.action {
case .quit:
appState.quit = true
}
return StateActionPair(state: appState, action: pair.action)
return (appState, tuple.action, true)
}
func reduceMainWindow(_ pair: MainWindowPair) -> MainWindowPair {
var appState = pair.state
let uuid = pair.action.uuid
class MainWindowReducer: ReducerType {
switch pair.action.payload {
typealias StateType = AppState
typealias ActionType = UuidAction<MainWindow.Action>
case let .becomeKey(isFullScreen):
appState.currentMainWindowUuid = uuid
appState.mainWindowTemplate = self.mainWindowTemplate(
from: appState.mainWindowTemplate,
new: appState.mainWindows[uuid] ?? appState.mainWindowTemplate,
isFullScreen: isFullScreen
)
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var appState = tuple.state
let uuid = tuple.action.uuid
switch tuple.action.payload {
case let .becomeKey(isFullScreen):
appState.currentMainWindowUuid = uuid
appState.mainWindowTemplate = self.mainWindowTemplate(
from: appState.mainWindowTemplate,
new: appState.mainWindows[uuid] ?? appState.mainWindowTemplate,
isFullScreen: isFullScreen
)
case let .frameChanged(to:frame):
if uuid == appState.currentMainWindowUuid {
appState.mainWindowTemplate.frame = frame
}
case let .setToolsState(tools):
appState.mainWindowTemplate.orderedTools = tools.map { $0.0 }
case let .toggleAllTools(value):
appState.mainWindowTemplate.isAllToolsVisible = value
case let .toggleToolButtons(value):
appState.mainWindowTemplate.isToolButtonsVisible = value
case .close:
if appState.currentMainWindowUuid == uuid, let mainWindowToClose = appState.mainWindows[uuid] {
appState.mainWindowTemplate = self.mainWindowTemplate(from: appState.mainWindowTemplate,
new: mainWindowToClose,
isFullScreen: false)
appState.currentMainWindowUuid = nil
}
appState.mainWindows.removeValue(forKey: uuid)
case let .setTheme(theme):
appState.mainWindowTemplate.appearance.theme = Marked(theme)
default:
return tuple
case let .frameChanged(to:frame):
if uuid == appState.currentMainWindowUuid {
appState.mainWindowTemplate.frame = frame
}
case let .setToolsState(tools):
appState.mainWindowTemplate.orderedTools = tools.map { $0.0 }
return (appState, tuple.action, true)
}
case let .toggleAllTools(value):
appState.mainWindowTemplate.isAllToolsVisible = value
private func mainWindowTemplate(from old: MainWindow.State,
new: MainWindow.State,
isFullScreen: Bool) -> MainWindow.State {
case let .toggleToolButtons(value):
appState.mainWindowTemplate.isToolButtonsVisible = value
var result = old
case .close:
if appState.currentMainWindowUuid == uuid, let mainWindowToClose = appState.mainWindows[uuid] {
appState.mainWindowTemplate = self.mainWindowTemplate(from: appState.mainWindowTemplate,
new: mainWindowToClose,
isFullScreen: false)
appState.currentMainWindowUuid = nil
if !isFullScreen {
result.frame = new.frame
}
appState.mainWindows.removeValue(forKey: uuid)
case let .setTheme(theme):
appState.mainWindowTemplate.appearance.theme = Marked(theme)
default:
return pair
result.isAllToolsVisible = new.isAllToolsVisible
result.isToolButtonsVisible = new.isToolButtonsVisible
result.tools = new.tools
result.orderedTools = new.orderedTools
result.previewTool = new.previewTool
result.fileBrowserShowHidden = new.fileBrowserShowHidden
result.htmlPreview = .default
return result
}
return StateActionPair(state: appState, action: pair.action)
}
private func mainWindowTemplate(from old: MainWindow.State,
new: MainWindow.State,
isFullScreen: Bool) -> MainWindow.State {
var result = old
if !isFullScreen {
result.frame = new.frame
}
result.isAllToolsVisible = new.isAllToolsVisible
result.isToolButtonsVisible = new.isToolButtonsVisible
result.tools = new.tools
result.orderedTools = new.orderedTools
result.previewTool = new.previewTool
result.fileBrowserShowHidden = new.fileBrowserShowHidden
result.htmlPreview = .default
return result
}
}

View File

@ -5,6 +5,7 @@ import subprocess
import argparse
import os
import uuid
import json
class Action:
@ -17,13 +18,15 @@ class Action:
class QueryParamKey:
PIPE_PATH = "pipe-path"
ENV_PATH = "env-path"
CWD = "cwd"
FILE = "file"
NVIM_ARGS = "nvim-args"
WAIT = "wait"
LINE = "line"
def wait_for_ui_to_close():
def wait_for_ui_to_close(pipe_path):
with open(pipe_path, 'r') as fifo:
while True:
if len(fifo.read()) == 0:
@ -31,7 +34,6 @@ def wait_for_ui_to_close():
def call_open(action, query_params, args):
query_params[QueryParamKey.PIPE_PATH] = pipe_path
if args.wait:
query_params[QueryParamKey.WAIT] = "true"
@ -47,10 +49,8 @@ def abspath(path):
return os.path.abspath(os.path.expanduser(path))
def vimr_nvim(other_args, nvim_args):
query_params = {
QueryParamKey.CWD: os.getcwd()
}
def vimr_nvim(other_args, nvim_args, query_params):
query_params[QueryParamKey.CWD] = os.getcwd()
if nvim_args:
query_params[QueryParamKey.NVIM_ARGS] = nvim_args
@ -58,14 +58,12 @@ def vimr_nvim(other_args, nvim_args):
call_open(Action.NVIM, query_params, other_args)
def vimr(action, args):
def vimr(action, args, query_params):
cwd = os.getcwd()
if args.cwd is not None:
cwd = abspath(args.cwd)
query_params = {
QueryParamKey.CWD: cwd
}
query_params[QueryParamKey.CWD] = cwd
files = args.file
if files:
@ -74,67 +72,99 @@ def vimr(action, args):
call_open(action, query_params, args)
pipe_path = "/tmp/com_qvacua_vimr_cli_pipe_{0}".format(str(uuid.uuid4()))
def main(args):
uuid_str = str(uuid.uuid4())
pipe_path = "/tmp/com_qvacua_vimr_cli_pipe_{0}".format(uuid_str)
if os.path.exists(pipe_path):
os.remove(pipe_path)
try:
os.mkfifo(pipe_path, 0600)
except OSError as error:
print("ERROR: {0}\n"
"{1} could not be mkfifo'ed.\n"
"Please go to https://github.com/qvacua/vimr and create an issue.".format(error, pipe_path))
raise
query_params = {
QueryParamKey.PIPE_PATH: pipe_path
}
if args.line is not None:
query_params[QueryParamKey.LINE] = args.line
if args.cur_env:
env_file = "/tmp/com_qvacua_vimr_env_{0}".format(uuid_str)
with open(env_file, "w") as f:
f.write(json.dumps({k: v for (k, v) in os.environ.items()}))
os.chmod(env_file, 0600)
query_params[QueryParamKey.ENV_PATH] = env_file
if args.nvim:
nvim_parser = argparse.ArgumentParser()
nvim_parser.add_argument("--nvim", action="store_true")
nvim_parser.add_argument("--wait", action="store_true")
nvim_parser.add_argument("--cur-env", action="store_true")
nvim_parser.add_argument("--dry-run", action="store_true")
nvim_parser.add_argument("--line", action="store")
other_args, nvim_args = nvim_parser.parse_known_args()
vimr_nvim(other_args, nvim_args, query_params)
else:
if not args.file:
action = Action.ACTIVATE
elif args.new_window:
action = Action.NEW_WINDOW
elif args.separate_windows:
action = Action.SEPARATE_WINDOWS
else:
action = Action.OPEN
vimr(action, args, query_params)
if args.dry_run:
exit(0)
wait_for_ui_to_close(pipe_path)
if os.path.exists(pipe_path):
os.remove(pipe_path)
try:
os.mkfifo(pipe_path, 0600)
except OSError as error:
print("ERROR: {0}\n"
"{1} could not be mkfifo'ed.\n"
"Please go to https://github.com/qvacua/vimr and create an issue.".format(error, pipe_path))
raise
description = """
def parse_args():
description = """
Open files in VimR: By default all files are open in tabs in the front most window or in a new window if there is none.
The working directory will be set to the current directory.
"""
parser = argparse.ArgumentParser(description=description)
parser = argparse.ArgumentParser(description=description)
parser.add_argument("--dry-run", action="store_true", dest="dry_run", help="Just print the 'open' command.")
parser.add_argument("--cwd", action="store", help="Set the working directory.")
parser.add_argument("--wait",
action="store_true",
help="This command line tool will exit when the corresponding UI window is closed.")
parser.add_argument("--nvim",
parser.add_argument("--dry-run", action="store_true", dest="dry_run", help="Just print the 'open' command.")
parser.add_argument("--cwd", action="store", help="Set the working directory.")
parser.add_argument("--line", action="store", help="Go to line")
parser.add_argument("--wait",
action="store_true",
help="All other arguments (except --dry-run and --wait) will be passed over to nvim.")
help="This command line tool will exit when the corresponding UI window is closed.")
parser.add_argument("--nvim",
action="store_true",
help="All arguments except --cur-env, --line, --dry-run and --wait will be passed "
"over to the (new) nvim instance in a new UI window.")
group = parser.add_mutually_exclusive_group()
# no option => Open files in tabs in the front most window.
group.add_argument("-n", action="store_true", dest="new_window", help="Open files in tabs in a new window.")
group.add_argument("-s", action="store_true", dest="separate_windows", help="Open files in separate windows.")
group = parser.add_mutually_exclusive_group()
# no option => Open files in tabs in the front most window.
group.add_argument("--cur-env",
action="store_true",
dest="cur_env",
help="Use the current environment variables when launching the background neovim process. "
"All files will be opened in a new window.")
group.add_argument("-n", action="store_true", dest="new_window", help="Open files in tabs in a new window.")
group.add_argument("-s", action="store_true", dest="separate_windows", help="Open files in separate windows.")
parser.add_argument("file", nargs="*")
parser.add_argument("file", nargs="*")
args, _ = parser.parse_known_args()
args, _ = parser.parse_known_args()
return args
if args.nvim:
nvim_parser = argparse.ArgumentParser()
nvim_parser.add_argument("--nvim", action="store_true")
nvim_parser.add_argument("--wait", action="store_true")
nvim_parser.add_argument("--dry-run", action="store_true")
other_args, nvim_args = nvim_parser.parse_known_args()
vimr_nvim(other_args, nvim_args)
else:
if not args.file:
action = Action.ACTIVATE
elif args.new_window:
action = Action.NEW_WINDOW
elif args.separate_windows:
action = Action.SEPARATE_WINDOWS
else:
action = Action.OPEN
vimr(action, args)
if args.dry_run:
exit(0)
wait_for_ui_to_close()
os.remove(pipe_path)
if __name__ == "__main__":
args = parse_args()
main(args)

View File

@ -15,10 +15,10 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>0.24.0</string>
<string>SNAPSHOT-284</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>282</string>
<string>284</string>
</dict>
</plist>

View File

@ -7,35 +7,22 @@
<description>Most recent changes with links to updates for VimR.</description>
<language>en</language>
<item>
<title>v0.24.0-282</title>
<title>SNAPSHOT-284</title>
<description><![CDATA[
<ul>
<li>Neovim 0.3.0</li>
<li>Some refactorings for the Neovim and the UI interface.</li>
<li>GH-402: Add file associations; using definitions and icons from <a href="http://macvim.org/">MacVim</a></li>
<li>GH-636: Bugfix: double cursor when entering terminal</li>
<li>GH-653: Bugfix: Crashes when closing the last window with "Quit after last window closes"-option turned on.</li>
<li>Bugfix: Crashes when <code>vimr --wait</code> is used, but is <code>Ctlr-C</code>'ed before closing the UI window.</li>
<li>Bugfix: <code>vimr --wait SOME_FILE</code> does not exit.</li>
<li>Use LuaJIT again.</li>
<li>Dependencies updates:<ul>
<li>sparkle-project/Sparkle@1.19.0</li>
<li>Quick/nimble@7.1.2</li>
<li>eonil/FileSystemEvents@1.0.0</li>
</ul>
</li>
<li>GH-443: <code>vimr --line ${LINE_NUMBER} ${SOME_FILE}</code> will open the file and go to the given line. If the file is already open in a UI window, then that window will be selected and the cursor will be moved to the given line. This can be used e.g. to reverse-search LaTeX. Note that you have to re-install the <code>vimr</code> tool.</li>
</ul>
]]></description>
<releaseNotesLink>
https://github.com/qvacua/vimr/releases/tag/v0.24.0-282
https://github.com/qvacua/vimr/releases/tag/snapshot/284
</releaseNotesLink>
<pubDate>2018-07-02T19:16:46.907124</pubDate>
<pubDate>2018-07-06T21:01:36.974431</pubDate>
<minimumSystemVersion>10.10.0</minimumSystemVersion>
<enclosure url="https://github.com/qvacua/vimr/releases/download/v0.24.0-282/VimR-v0.24.0-282.tar.bz2"
sparkle:version="282"
sparkle:shortVersionString="0.24.0"
sparkle:dsaSignature="MCwCFDolgerESMrUpxwmcdVot47aoPiHAhQ0qFQcMrpyTZ23onJ/zv8XaaHHSA=="
length="14119123"
<enclosure url="https://github.com/qvacua/vimr/releases/download/snapshot/284/VimR-SNAPSHOT-284.tar.bz2"
sparkle:version="284"
sparkle:shortVersionString="SNAPSHOT-284"
sparkle:dsaSignature="MC4CFQC85W5DOK+chBghYPusLQGPdfrXWAIVAOKVRd3YSnGyXJ3w5S3F7LU4vJTM"
length="14182159"
type="application/octet-stream"/>
</item>
</channel>

View File

@ -1,3 +1,12 @@
# 0.25.0-???
* GH-625: `vimr --cur-env` will pass the current environment variables to the new neovim process. This will result in `virtualenv` support.
* GH-443: `vimr --line ${LINE_NUMBER} ${SOME_FILE}` will open the file and go to the given line. If the file is already open in a UI window, then that window will be selected and the cursor will be moved to the given line. This can be used e.g. to reverse-search LaTeX.
* Dependencies updates:
- ReactiveX/RxSwift@4.2.0
- httpswift/swifter@1.4.2
- `MMCoreTextView` of Macvim: macvim-dev/macvim@351faf929e4abe32ea4cc31078d1a625fc86a69f
# 0.24.0-282
* Neovim 0.3.0