Compare commits

..

No commits in common. "master" and "1.0.0" have entirely different histories.

502 changed files with 16116 additions and 84480 deletions

3
.gitattributes vendored
View File

@ -1,3 +0,0 @@
static/js/lib/* linguist-vendored
static/js/overwrite/* linguist-vendored
*.min.js linguist-vendored

15
.gitignore vendored
View File

@ -1,12 +1,11 @@
/wide.exe /wide.exe
/wide /wide
/main
/header /data/workspace/bin/
/header.exe /data/workspace/pkg/
/data/workspace/src/
/**/.DS_Store /data/user_workspaces/*/bin/
/data/user_workspaces/*/pkg/
/node_modules /data/user_workspaces/*/src/**/*.exe
/nbproject
/workspaces
.idea

22
.gobuild.yml Normal file
View File

@ -0,0 +1,22 @@
author: DL88250@gmail.com
description: A Web IDE for Teams using Golang.
filesets:
depth: 10
includes:
- conf
- data
- doc
- i18n
- static
- views
- README.md
- LICENSE
excludes:
- \.git
settings:
targetdir: ""
build: |
test -d Godeps && go(){ godep go "$@";} ; go install -v
outfiles:
- wide

View File

@ -1,21 +0,0 @@
{
"Dir": ".",
"Template": ".header.txt",
"Includes": [
"*.go",
"*/*.go",
"static/css/*.css",
"static/js/*.js",
"static/js/overwrite/*.js"
],
"Excludes": [
"static/js/lib/*",
"static/user/*",
"vendor/**"
],
"UseDefaultExcludes": true,
"Properties": {
"Year": "2014-present",
"Owner": "b3log.org"
}
}

View File

@ -1,13 +0,0 @@
Copyright (c) {{.Year}}, {{.Owner}}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,4 +0,0 @@
language: go
go:
- 1.12

View File

@ -192,7 +192,7 @@
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,

136
README.md
View File

@ -1,18 +1,124 @@
* [Wide 用户指南](https://ld246.com/article/1538873544275) # Wide [![Build Status](https://drone.io/github.com/b3log/wide/status.png)](https://drone.io/github.com/b3log/wide/latest)
* [Wide 开发指南](https://ld246.com/article/1538876422995)
![Overview](https://cloud.githubusercontent.com/assets/873584/5450620/1d51831e-8543-11e4-930b-670871902425.png)
![Goto File](https://cloud.githubusercontent.com/assets/873584/5450616/1d495da6-8543-11e4-9285-f9d9c60779ac.png)
![Autocomplete](https://cloud.githubusercontent.com/assets/873584/5450619/1d4d5712-8543-11e4-8fe4-35dbc8348a6e.png)
![Theme](https://cloud.githubusercontent.com/assets/873584/5450617/1d4c0826-8543-11e4-8b86-f79a4e41550a.png) ## Intro
![Show Expression Info](https://cloud.githubusercontent.com/assets/873584/5450618/1d4cd9f4-8543-11e4-950f-121bd3ff4a39.png)
![Build Error Info](https://cloud.githubusercontent.com/assets/873584/5450632/3e51cccc-8543-11e4-8ca8-8d2427aa16b8.png)
![Cross-Compilation](https://cloud.githubusercontent.com/assets/873584/10130037/226d75fc-65f7-11e5-94e4-25ee579ca175.png) A <b>W</b>eb <b>IDE</b> for Teams using Golang.
![Playground](https://cloud.githubusercontent.com/assets/873584/21209772/449ecfd2-c2b1-11e6-9aa6-a83477d9f269.gif) <img src="https://cloud.githubusercontent.com/assets/873584/4606377/d0ca3c2a-521b-11e4-912c-d955ab05850b.png" width="100%" />
## Motivation
* **Team** IDE:
* Safe and reliable: the project source code stored on the server in real time, the developer's machine crashes without losing any source code
* Unified environment: server unified development environment configuration, the developer machine without any additional configuration
* Out of the box: 5 minutes to setup a server then open browser to develop, debug
* Version Control: each developer has its own source code repository, easy sync with the trunk
* **Web based** IDE:
* Developer needs a browser only
* Cross-platform, even on mobile devices
* Easy for extensions
* Easy integration with other systems
* For the geeks
* A try for commercial-open source: versions customized for enterprises, close to their development work flows respectively
* Currently more popular Go IDE has some defects or regrets:
* Text editor (vim/emacs/sublime/Atom, etc.): For the Go newbie is too complex
* Plug-in (goclipse, etc.): the need for the original IDE support, not professional
* LiteIDE: no modern user interface :p
* No team development experience
* There are a few of GO IDEs, and no one developed by Go itself, this is a nice try
## Features
* Code Highlight, Folding: Go/HTML/JavaScript/Markdown etc.
* Autocomplete: Go/HTML etc.
* Format: Go/HTML/JSON etc.
* Run & Debug: run/debug multiple processes at the same time
* Multiplayer: a real team development experience
* Navigation, Jump to declaration, Find usages, File search etc.
* Shell: run command on the server
* Git integration: git command on the web
* Web development: Frontend devlopment (HTML/JS/CSS) all in one
* Go tool: go get/install/fmt etc.
## Architecture
### Build & Run
![Build & Run](https://cloud.githubusercontent.com/assets/873584/4389219/3642bc62-43f3-11e4-8d1f-06d7aaf22784.png)
* A browser tab corresponds to a Wide session
* Execution output push via WebSocket
Flow:
1. Browser sends ````Build```` request
2. Server executes ````go build```` command via ````os/exec````<br/>
2.1. Generates a executable file
3. Browser sends ````Run```` request
4. Server executes the file via ````os/exec````<br/>
4.1. A running process<br/>
4.2. Execution output push via WebSocket channel
5. Browser renders with callback function ````ws.onmessage````
### Code Assist
![Code Assist](https://cloud.githubusercontent.com/assets/873584/4399135/3b80c21c-4463-11e4-8e94-7f7e8d12a4df.png)
* Autocompletion
* Find Usages
Flow:
1. Browser sends code assist request
2. Handler gets user workspace of the request with HTTP session
3. Server executes ````gocode````/````ide_stub````<br/>
3.1 Sets environment variables (e.g. ${GOPATH})<br/>
3.2 ````gocode```` with ````lib-path```` parameter
## Documents
* [用户指南](http://88250.gitbooks.io/wide-user-guide)
* [开发指南](http://88250.gitbooks.io/wide-dev-guide)
## Demos
* 20141024-1.0.0, png ![](http://b3log.org/wide/demo/20141024.png)
### Olds
* [20140927, png](http://b3log.org/wide/demo/20140927.png)
* [20140913, png](http://b3log.org/wide/demo/20140913.png)
* [20140910, png](http://b3log.org/wide/demo/20140910.png)
* [20140823, swf](http://b3log.org/wide/demo/20140823.html)
## Setup from sources
1. Downloads source
2. Gets dependencies with
* `go get -u`
* `go get -u github.com/88250/ide_stub`
* `go get -u github.com/nsf/gocode`
3. Compiles wide with `go build`
4. Configures `conf/wide.json`
5. Runs the executable `wide` or `wide.exe`
## Known Issues
* [Shell is not available on Windows](https://github.com/b3log/wide/issues/32)
## License
Copyright (c) 2014, B3log Team (http://b3log.org)
Licensed under the [Apache License 2.0](https://github.com/b3log/wide/blob/master/LICENSE).
## Credits
* [golang](http://golang.org)
* [CodeMirror](https://github.com/marijnh/CodeMirror)
* [zTree](https://github.com/zTree/zTree_v3)
* [LiteIDE](https://github.com/visualfc/liteide)
* [gocode](https://github.com/nsf/gocode)
* [Gorilla](https://github.com/gorilla)
----
<img src="https://cloud.githubusercontent.com/assets/873584/4606328/4e848b96-5219-11e4-8db1-fa12774b57b4.png" width="256px" />
</center>

View File

@ -1,4 +0,0 @@
* This software is open sourced under the Apache License 2.0
* You can not get rid of the "Powered by [B3log](https://b3log.org)" from any pages, even the pages are developed by you
* If you want to use this software for commercial purpose, please mail to os@b3log.org for request a commercial license
* Copyright (c) b3log.org, all rights reserved

View File

@ -1,162 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package conf
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
// Panel represents a UI panel.
type Panel struct {
State string `json:"state"` // panel state, "min"/"max"/"normal"
Size uint16 `json:"size"` // panel size
}
// Layout represents the UI layout.
type Layout struct {
Side *Panel `json:"side"` // Side panel
SideRight *Panel `json:"sideRight"` // Right-Side panel
Bottom *Panel `json:"bottom"` // Bottom panel
}
// LatestSessionContent represents the latest session content.
type LatestSessionContent struct {
FileTree []string `json:"fileTree"` // paths of expanding nodes of file tree
Files []string `json:"files"` // paths of files of opening editor tabs
CurrentFile string `json:"currentFile"` // path of file of the current focused editor tab
Layout *Layout `json:"layout"` // UI Layout
}
// User configuration.
type User struct {
Id string
Name string
Avatar string
Workspace string // the GOPATH of this user (maybe contain several paths splitted by os.PathListSeparator)
Locale string
GoFormat string
GoBuildArgsForLinux string
GoBuildArgsForWindows string
GoBuildArgsForDarwin string
FontFamily string
FontSize string
Theme string
Keymap string // wide/vim
Created int64 // user create time in unix nano
Updated int64 // preference update time in unix nano
Lived int64 // the latest session activity in unix nano
Editor *editor
LatestSessionContent *LatestSessionContent
}
// Editor configuration of a user.
type editor struct {
FontFamily string
FontSize string
LineHeight string
Theme string
TabSize string
}
// Save saves the user's configurations in conf/users/{userId}.json.
func (u *User) Save() bool {
bytes, err := json.MarshalIndent(u, "", " ")
if nil != err {
logger.Error(err)
return false
}
if "" == string(bytes) {
logger.Error("Truncated user [" + u.Id + "]")
return false
}
if err = ioutil.WriteFile(filepath.Join(Wide.Data, "users", u.Id+".json"), bytes, 0644); nil != err {
logger.Error(err)
return false
}
return true
}
// NewUser creates a user with the specified username and workspace.
func NewUser(id, name, avatar, workspace string) *User {
now := time.Now().UnixNano()
return &User{Id: id, Name: name, Avatar: avatar, Workspace: workspace,
Locale: Wide.Locale, GoFormat: "gofmt",
GoBuildArgsForLinux: "", GoBuildArgsForWindows: "", GoBuildArgsForDarwin: "",
FontFamily: "Helvetica", FontSize: "13px", Theme: "default",
Keymap: "wide",
Created: now, Updated: now, Lived: now,
Editor: &editor{FontFamily: "Consolas, 'Courier New', monospace", FontSize: "inherit", LineHeight: "17px",
Theme: "wide", TabSize: "4"}}
}
// WorkspacePath gets workspace path of the user.
//
// Compared to the use of Wide.Workspace, this function will be processed as follows:
// 1. Replace {WD} variable with the actual directory path
// 2. Replace ${GOPATH} with enviorment variable GOPATH
// 3. Replace "/" with "\\" (Windows)
func (u *User) WorkspacePath() string {
w := u.Workspace
w = strings.Replace(w, "${GOPATH}", os.Getenv("GOPATH"), 1)
return filepath.FromSlash(w)
}
// BuildArgs get build args with the specified os.
func (u *User) BuildArgs(os string) []string {
var tmp string
if os == "windows" {
tmp = u.GoBuildArgsForWindows
}
if os == "linux" {
tmp = u.GoBuildArgsForLinux
}
if os == "darwin" {
tmp = u.GoBuildArgsForDarwin
}
exp := regexp.MustCompile(`[^\s"']+|"([^"]*)"|'([^']*)'`)
ret := exp.FindAllString(tmp, -1)
for idx := range ret {
ret[idx] = strings.Replace(ret[idx], "\"", "", -1)
}
return ret
}
// GetOwner gets the user the specified path belongs to. Returns "" if not found.
func GetOwner(path string) string {
for _, user := range Users {
if strings.HasPrefix(path, user.WorkspacePath()) {
return user.Id
}
}
return ""
}

View File

@ -1,331 +1,147 @@
// Copyright (c) 2014-present, b3log.org // Wide 配置相关,所有配置(包括用户配置)都是保存在 wide.json 中.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package conf includes configurations related manipulations.
package conf package conf
import ( import (
"encoding/json" "encoding/json"
"html/template" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sort" "runtime"
"strconv"
"strings" "strings"
"time" "time"
"github.com/88250/gulu" "github.com/b3log/wide/event"
"github.com/88250/wide/event" _ "github.com/b3log/wide/i18n"
"github.com/b3log/wide/util"
"github.com/golang/glog"
) )
const ( const (
// PathSeparator holds the OS-specific path separator. PathSeparator = string(os.PathSeparator) // 系统文件路径分隔符
PathSeparator = string(os.PathSeparator) PathListSeparator = string(os.PathListSeparator) // 系统路径列表分隔符
// PathListSeparator holds the OS-specific path list separator.
PathListSeparator = string(os.PathListSeparator)
// WideVersion holds the current Wide's version.
WideVersion = "1.6.0"
// CodeMirrorVer holds the current editor version.
CodeMirrorVer = "5.1"
// UserAgent represents HTTP client user agent.
UserAgent = "Wide/" + WideVersion + "; +https://github.com/88250/wide"
HelloWorld = `package main
import "fmt"
func main() {
fmt.Println("欢迎通过《边看边练 Go 系列》来学习 Go 语言 https://ld246.com/article/1437497122181")
}
`
) )
// Configuration. // 最后一次会话内容结构.
type LatestSessionContent struct {
FileTree []string // 文件树展开的路径集
Files []string // 编辑器打开的文件路径集
CurrentFile string // 当前编辑器文件路径
}
// 用户结构.
type User struct {
Name string
Password string
Workspace string // 该用户的工作空间 GOPATH 路径
Locale string
LatestSessionContent *LatestSessionContent
}
// 配置结构.
type conf struct { type conf struct {
Server string // server Server string // 服务地址({IP}:7070
LogLevel string // logging level: trace/debug/info/warn/error StaticServer string // 静态资源服务地址http://{IP}:7070
Data string // data directory EditorChannel string // 编辑器通道地址ws://{IP}:7070
RuntimeMode string // runtime mode (dev/prod) OutputChannel string // 输出窗口通道地址ws://{IP}:7070
HTTPSessionMaxAge int // HTTP session max age (in seciond) ShellChannel string // Shell 通道地址ws://{IP}:7070
StaticResourceVersion string // version of static resources SessionChannel string // Wide 会话通道地址ws://{IP}:7070
Locale string // default locale HTTPSessionMaxAge int // HTTP 会话失效时间(秒)
Autocomplete bool // default autocomplete StaticResourceVersion string // 静态资源版本
SiteStatCode template.HTML // site statistic code MaxProcs int // 并发执行数
ReadOnly bool // read-only mode RuntimeMode string // 运行模式
OAuthLoginURL string Pwd string // 工作目录
OAuthAccessTokenURL string Workspace string // 主工作空间 GOPATH 路径
OAuthUserInfoURL string Locale string // 默认的区域
OAuthClientID string Users []*User // 用户集
OAuthClientSecret string
} }
// Logger. // 配置.
var logger = gulu.Log.NewLogger(os.Stdout) var Wide conf
// Wide configurations. // 维护非变化部分的配置.
var Wide *conf
// configurations of users.
var Users []*User
// Indicates whether Docker is available.
var Docker bool
// Docker image to run user's program
const DockerImageGo = "golang"
// Load loads the Wide configurations from wide.json and users' configurations from users/{userId}.json.
func Load(confPath, confData, confServer, confLogLevel, confReadOnly string, confSiteStatCode template.HTML) {
initWide(confPath, confData, confServer, confLogLevel, confReadOnly, confSiteStatCode)
initUsers()
cmd := exec.Command("docker", "version")
_, err := cmd.CombinedOutput()
if nil != err {
logger.Warnf("Not found 'docker' installed, running user's code will cause security problem")
} else {
Docker = true
}
}
func initUsers() {
f, err := os.Open(Wide.Data + PathSeparator + "users")
if nil != err {
logger.Error(err)
os.Exit(-1)
}
names, err := f.Readdirnames(-1)
if nil != err {
logger.Error(err)
os.Exit(-1)
}
f.Close()
for _, name := range names {
if strings.HasPrefix(name, ".") { // hiden files that not be created by Wide
continue
}
if ".json" != filepath.Ext(name) { // such as backup (*.json~) not be created by Wide
continue
}
user := &User{}
bytes, _ := os.ReadFile(filepath.Join(Wide.Data, "users", name))
err := json.Unmarshal(bytes, user)
if err != nil {
logger.Errorf("Parses [%s] error: %v, skip loading this user", name, err)
continue
}
// Compatibility upgrade (1.3.0): https://github.com/b3log/wide/issues/83
if "" == user.Keymap {
user.Keymap = "wide"
}
// Compatibility upgrade (1.5.3): https://github.com/b3log/wide/issues/308
//if "" == user.GoBuildArgsForLinux {
// user.GoBuildArgsForLinux = "-i"
//}
//if "" == user.GoBuildArgsForWindows {
// user.GoBuildArgsForWindows = "-i"
//}
//if "" == user.GoBuildArgsForDarwin {
// user.GoBuildArgsForDarwin = "-i"
//}
Users = append(Users, user)
}
initWorkspaceDirs()
initCustomizedConfs()
}
func initWide(confPath, confData, confServer, confLogLevel, confReadOnly string, confSiteStatCode template.HTML) {
bytes, err := os.ReadFile(confPath)
if nil != err {
logger.Error(err)
os.Exit(-1)
}
Wide = &conf{}
err = json.Unmarshal(bytes, Wide)
if err != nil {
logger.Error("Parses [wide.json] error: ", err)
os.Exit(-1)
}
Wide.Autocomplete = true // default to true
// Logging Level
gulu.Log.SetLevel(Wide.LogLevel)
if "" != confLogLevel {
Wide.LogLevel = confLogLevel
gulu.Log.SetLevel(confLogLevel)
}
logger.Debug("Conf: \n" + string(bytes))
// User Home
home, err := gulu.OS.Home()
if nil != err {
logger.Error("Can't get user's home, please report this issue to developer", err)
os.Exit(-1)
}
logger.Debugf("${user.home} [%s]", home)
// Data directory
if "" != confData {
Wide.Data = confData
}
Wide.Data = strings.Replace(Wide.Data, "${home}", home, -1)
Wide.Data = filepath.Clean(Wide.Data)
if err := os.MkdirAll(Wide.Data+"/playground/", 0755); nil != err {
logger.Errorf("Create data directory [%s] error", err)
os.Exit(-1)
}
if err := os.MkdirAll(Wide.Data+"/users/", 0755); nil != err {
logger.Errorf("Create data directory [%s] error", err)
os.Exit(-1)
}
if err := os.MkdirAll(Wide.Data+"/workspaces/", 0755); nil != err {
logger.Errorf("Create data directory [%s] error", err)
os.Exit(-1)
}
// Server
if "" != confServer {
Wide.Server = confServer
}
if "" != confReadOnly {
Wide.ReadOnly, _ = strconv.ParseBool(confReadOnly)
}
// SiteStatCode
if "" != confSiteStatCode {
Wide.SiteStatCode = confSiteStatCode
}
time := strconv.FormatInt(time.Now().UnixNano(), 10)
logger.Debugf("${time} [%s]", time)
Wide.StaticResourceVersion = strings.Replace(Wide.StaticResourceVersion, "${time}", time, 1)
}
// FixedTimeCheckEnv checks Wide runtime enviorment periodically (7 minutes).
// //
// Exits process if found fatal issues (such as not found $GOPATH), // 只有 Users 是会运行时变化的,保存回写文件时要使用这个变量.
// Notifies user by notification queue if found warning issues (such as not found gocode). var rawWide conf
func FixedTimeCheckEnv() {
checkEnv() // check immediately
// 定时检查 Wide 运行环境.
//
// 如果是特别严重的问题(比如 $GOPATH 不存在)则退出进程,另一些不太严重的问题(比如 gocode 不存在)则放入全局通知队列.
func FixedTimeCheckEnv() {
go func() { go func() {
// 7 分钟进行一次检查环境
for _ = range time.Tick(time.Minute * 7) { for _ = range time.Tick(time.Minute * 7) {
checkEnv() if "" == os.Getenv("GOPATH") {
glog.Fatal("Not found $GOPATH")
os.Exit(-1)
}
if "" == os.Getenv("GOROOT") {
glog.Fatal("Not found $GOROOT")
os.Exit(-1)
}
gocode := Wide.GetGocode()
cmd := exec.Command(gocode, "close")
_, err := cmd.Output()
if nil != err {
event.EventQueue <- event.EvtCodeGocodeNotFound
glog.Warningf("Not found gocode [%s]", gocode)
}
ide_stub := Wide.GetIDEStub()
cmd = exec.Command(ide_stub, "version")
_, err = cmd.Output()
if nil != err {
event.EventQueue <- event.EvtCodeIDEStubNotFound
glog.Warningf("Not found ide_stub [%s]", ide_stub)
}
} }
}() }()
} }
func checkEnv() { // 定时1 分钟)保存配置.
defer gulu.Panic.Recover(nil) //
// 主要目的是保存用户会话内容,以备下一次用户打开 Wide 时进行会话还原.
cmd := exec.Command("go", "version") func FixedTimeSave() {
buf, err := cmd.CombinedOutput() go func() {
if nil != err { // 1 分钟进行一次配置保存
logger.Error("Not found 'go' command, please make sure Go has been installed correctly") for _ = range time.Tick(time.Minute) {
Save()
os.Exit(-1) }
} }()
logger.Trace(string(buf))
if "" == os.Getenv("GOPATH") {
logger.Error("Not found $GOPATH, please configure it before running Wide")
os.Exit(-1)
}
gocode := gulu.Go.GetExecutableInGOBIN("gocode")
cmd = exec.Command(gocode)
_, err = cmd.Output()
if nil != err {
event.EventQueue <- &event.Event{Code: event.EvtCodeGocodeNotFound}
logger.Warnf("Not found gocode [%s], please install it with this command: go get github.com/stamblerre/gocode", gocode)
}
ideStub := gulu.Go.GetExecutableInGOBIN("gotools")
cmd = exec.Command(ideStub, "version")
_, err = cmd.Output()
if nil != err {
event.EventQueue <- &event.Event{Code: event.EvtCodeIDEStubNotFound}
logger.Warnf("Not found gotools [%s], please install it with this command: go get github.com/visualfc/gotools", ideStub)
}
} }
// GetUserWorkspace gets workspace path with the specified user id, returns "" if not found. // 获取 username 指定的用户的工作空间路径,查找不到时返回空字符串.
func GetUserWorkspace(userId string) string { func (c *conf) GetUserWorkspace(username string) string {
for _, user := range Users { for _, user := range c.Users {
if user.Id == userId { if user.Name == username {
return user.WorkspacePath() ret := strings.Replace(user.Workspace, "{pwd}", c.Pwd, 1)
return filepath.FromSlash(ret)
} }
} }
return "" return ""
} }
// GetGoFmt gets the path of Go format tool, returns "gofmt" if not found "goimports". // 获取主工作空间路径.
func GetGoFmt(userId string) string { func (c *conf) GetWorkspace() string {
for _, user := range Users { return filepath.FromSlash(strings.Replace(c.Workspace, "{pwd}", c.Pwd, 1))
if user.Id == userId {
switch user.GoFormat {
case "gofmt":
return "gofmt"
case "goimports":
return gulu.Go.GetExecutableInGOBIN("goimports")
default:
logger.Errorf("Unsupported Go Format tool [%s]", user.GoFormat)
return "gofmt"
}
}
}
return "gofmt"
} }
// GetUser gets configuration of the user specified by the given user id, returns nil if not found. // 获取 user 的工作空间路径.
func GetUser(id string) *User { func (user *User) getWorkspace() string {
if "playground" == id { // reserved user for Playground ret := strings.Replace(user.Workspace, "{pwd}", Wide.Pwd, 1)
return NewUser("playground", "playground", "", "")
}
for _, user := range Users { return filepath.FromSlash(ret)
if user.Id == id { }
// 获取 username 指定的用户配置.
func (*conf) GetUser(username string) *User {
for _, user := range Wide.Users {
if user.Name == username {
return user return user
} }
} }
@ -333,131 +149,110 @@ func GetUser(id string) *User {
return nil return nil
} }
// initCustomizedConfs initializes the user customized configurations. // 获取 gocode 路径.
func initCustomizedConfs() { func (*conf) GetGocode() string {
for _, user := range Users { return getGOBIN() + "gocode"
UpdateCustomizedConf(user.Id)
}
} }
// UpdateCustomizedConf creates (if not exists) or updates user customized configuration files. // 获取 ide_stub 路径.
// func (*conf) GetIDEStub() string {
// 1. /static/users/{userId}/style.css return getGOBIN() + "ide_stub"
func UpdateCustomizedConf(userId string) { }
var u *User
for _, user := range Users { // maybe it is a beauty of the trade-off of the another world between design and implementation // 获取 GOBIN 路径,末尾带路径分隔符.
if user.Id == userId { func getGOBIN() string {
u = user // $GOBIN/
} ret := os.Getenv("GOBIN")
if "" != ret {
return ret + PathSeparator
} }
if nil == u { // $GOPATH/bin/$GOOS_$GOARCH/
return ret = os.Getenv("GOPATH") + PathSeparator + "bin" + PathSeparator +
os.Getenv("GOOS") + "_" + os.Getenv("GOARCH")
if isExist(ret) {
return ret + PathSeparator
} }
model := map[string]interface{}{"user": u} // $GOPATH/bin/{runtime.GOOS}_{runtime.GOARCH}/
ret = os.Getenv("GOPATH") + PathSeparator + "bin" + PathSeparator +
runtime.GOOS + "_" + runtime.GOARCH
if isExist(ret) {
return ret + PathSeparator
}
// $GOPATH/bin/
return os.Getenv("GOPATH") + PathSeparator + "bin" + PathSeparator
}
// 保存 Wide 配置.
func Save() bool {
// 只有 Users 是会运行时变化的,其他属性只能手工维护 wide.json 配置文件
rawWide.Users = Wide.Users
// 原始配置文件内容
bytes, err := json.MarshalIndent(rawWide, "", " ")
t, err := template.ParseFiles("static/user/style.css.tmpl")
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
os.Exit(-1) return false
} }
dir := filepath.Clean(Wide.Data + "/static/users/" + userId) if err = ioutil.WriteFile("conf/wide.json", bytes, 0644); nil != err {
if err := os.MkdirAll(dir, 0755); nil != err { glog.Error(err)
logger.Error(err)
os.Exit(-1) return false
} }
fout, err := os.Create(dir + PathSeparator + "style.css") return true
if nil != err {
logger.Error(err)
os.Exit(-1)
}
defer fout.Close()
if err := t.Execute(fout, model); nil != err {
logger.Error(err)
os.Exit(-1)
}
} }
// initWorkspaceDirs initializes the directories of users' workspaces. // 加载 Wide 配置.
func Load() {
bytes, _ := ioutil.ReadFile("conf/wide.json")
err := json.Unmarshal(bytes, &Wide)
if err != nil {
glog.Error(err)
os.Exit(-1)
}
// 保存未经变量替换处理的原始配置文件,用于写回时
json.Unmarshal(bytes, &rawWide)
ip, err := util.Net.LocalIP()
if err != nil {
glog.Error(err)
os.Exit(-1)
}
glog.V(3).Infof("IP [%s]", ip)
Wide.Server = strings.Replace(Wide.Server, "{IP}", ip, 1)
Wide.StaticServer = strings.Replace(Wide.StaticServer, "{IP}", ip, 1)
Wide.EditorChannel = strings.Replace(Wide.EditorChannel, "{IP}", ip, 1)
Wide.OutputChannel = strings.Replace(Wide.OutputChannel, "{IP}", ip, 1)
Wide.ShellChannel = strings.Replace(Wide.ShellChannel, "{IP}", ip, 1)
Wide.SessionChannel = strings.Replace(Wide.SessionChannel, "{IP}", ip, 1)
// 获取当前执行路径
file, _ := exec.LookPath(os.Args[0])
pwd, _ := filepath.Abs(file)
pwd = pwd[:strings.LastIndex(pwd, PathSeparator)]
Wide.Pwd = pwd
glog.V(3).Infof("pwd [%s]", pwd)
glog.V(3).Info("Conf: \n" + string(bytes))
}
// 检查文件或目录是否存在.
// //
// Creates directories if not found on path of workspace. // 如果由 filename 指定的文件或目录存在则返回 true否则返回 false.
func initWorkspaceDirs() { func isExist(filename string) bool {
paths := []string{} _, err := os.Stat(filename)
for _, user := range Users { return err == nil || os.IsExist(err)
paths = append(paths, filepath.SplitList(user.WorkspacePath())...)
}
for _, path := range paths {
CreateWorkspaceDir(path)
}
}
// CreateWorkspaceDir creates (if not exists) directories on the path.
//
// 1. root directory:{path}
// 2. src directory: {path}/src
// 3. package directory: {path}/pkg
// 4. binary directory: {path}/bin
func CreateWorkspaceDir(path string) {
createDir(path)
createDir(path + PathSeparator + "src")
createDir(path + PathSeparator + "pkg")
createDir(path + PathSeparator + "bin")
}
// createDir creates a directory on the path if it not exists.
func createDir(path string) {
if !gulu.File.IsExist(path) {
if err := os.MkdirAll(path, 0775); nil != err {
logger.Error(err)
os.Exit(-1)
}
logger.Tracef("Created a dir [%s]", path)
}
}
// GetEditorThemes gets the names of editor themes.
func GetEditorThemes() []string {
ret := []string{}
f, _ := os.Open("static/js/overwrite/codemirror" + "/theme")
names, _ := f.Readdirnames(-1)
f.Close()
for _, name := range names {
ret = append(ret, name[:strings.LastIndex(name, ".")])
}
sort.Strings(ret)
return ret
}
// GetThemes gets the names of themes.
func GetThemes() []string {
ret := []string{}
f, _ := os.Open("static/css/themes")
names, _ := f.Readdirnames(-1)
f.Close()
for _, name := range names {
ret = append(ret, name[:strings.LastIndex(name, ".")])
}
sort.Strings(ret)
return ret
} }

View File

@ -1,16 +1,28 @@
{ {
"Server": "http://127.0.0.1:7070", "Server": "{IP}:7070",
"LogLevel": "debug", "StaticServer": "http://{IP}:7070",
"Data": "${home}/wide", "EditorChannel": "ws://{IP}:7070",
"RuntimeMode": "prod", "OutputChannel": "ws://{IP}:7070",
"HTTPSessionMaxAge": 86400, "ShellChannel": "ws://{IP}:7070",
"StaticResourceVersion": "${time}", "SessionChannel": "ws://{IP}:7070",
"Locale": "zh_CN", "HTTPSessionMaxAge": 86400,
"SiteStatCode": "", "StaticResourceVersion": "201410241024",
"ReadOnly": false, "MaxProcs": 4,
"OAuthLoginURL": "", "RuntimeMode": "dev",
"OAuthAccessTokenURL": "", "Pwd": "{pwd}",
"OAuthUserInfoURL": "", "Workspace": "{pwd}/data/workspace",
"OAuthClientID": "", "Locale": "zh_CN",
"OAuthClientSecret": "" "Users": [
{
"Name": "admin",
"Password": "admin",
"Workspace": "{pwd}/data/user_workspaces/admin",
"Locale": "zh_CN",
"LatestSessionContent": {
"FileTree": [],
"Files": [],
"CurrentFile": ""
}
}
]
} }

1
data/user_workspaces/README Executable file
View File

@ -0,0 +1 @@
User workspaces.

View File

@ -0,0 +1,9 @@
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello, 世界")
}

View File

@ -0,0 +1,411 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
{{.i18n.wide}}
</title>
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/codemirror.css">
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/hint/show-hint.css">
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/lint/lint.css">
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/fold/foldgutter.css">
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/theme/wide.css">
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/js/lib/ztree/zTreeStyle.css">
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/css/dialog.css?{{.conf.StaticResourceVersion}}">
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/css/base.css?{{.conf.StaticResourceVersion}}">
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/css/wide.css?{{.conf.StaticResourceVersion}}">
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/css/side.css?{{.conf.StaticResourceVersion}}">
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/css/start.css?{{.conf.StaticResourceVersion}}">
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
</head>
<body>
<!-- 主菜单 -->
<div class="menu fn-clear">
<ul class="fn-cleaer">
<li>
<span>
{{.i18n.file}}
</span>
<div class="frame">
<ul>
<li class="save-all disabled" onclick="wide.saveAllFiles()">
<span>
{{.i18n.save_all_files}}
</span>
</li>
<li class="close-all disabled" onclick="wide.closeAllFiles()">
<span>
{{.i18n.close_all_files}}
</span>
</li>
<li class="hr">
</li>
<li onclick="wide.exit()">
<span>
{{.i18n.exit}}
</span>
</li>
</ul>
</div>
</li>
<li>
<span>
{{.i18n.run}}
</span>
<div class="frame">
<ul>
<li class="run disabled" onclick="wide.run()">
<span>
{{.i18n.build_n_run}}
</span>
</li>
<li class="hr">
</li>
<li class="go-get disabled" onclick="wide.goget()">
<span>
{{.i18n.goget}}
</span>
</li>
<li class="go-install disabled" onclick="wide.goinstall()">
<span>
{{.i18n.goinstall}}
</span>
</li>
</ul>
</div>
</li>
<!--
<li>
<span>{{.i18n.debug}}</span>
<div class="frame">
<ul>
<li>
<span>{{.i18n.debug}}</span>
</li>
</ul>
</div>
</li>
-->
<li>
<span>
{{.i18n.help}}
</span>
<div class="frame">
<ul>
<li onclick="window.open('https://www.gitbook.io/book/88250/wide-user-guide')">
<span>
{{.i18n.wide_doc}}
</span>
</li>
<li onclick="window.open('https://github.com/b3log/wide/issues/new')">
{{.i18n.report_issues}}
</li>
<li class="hr">
</li>
<li onclick="editors.openStartPage()">
<span>
{{.i18n.start_page}}
</span>
</li>
<li onclick="wide.openAbout()">
<span>
{{.i18n.about}}
</span>
</li>
</ul>
</div>
</li>
</ul>
</div>
<div class="content">
<div class="side">
<span title="{{.i18n.min}}" class="font-ico ico-min">
</span>
<div class="tabs">
<div class="current" data-index="filreTree">
<span title="{{.i18n.file}}">
{{.i18n.file}}
</span>
</div>
</div>
<div class="tabs-panel">
<div data-index="filreTree">
<ul id="files" tabindex="-1" class="ztree">
</ul>
<!-- 目录右键菜单 -->
<div id="dirRMenu" class="frame">
<ul>
<li onclick="tree.newFile();">
{{.i18n.create_file}}
</li>
<li onclick="tree.newDir();">
{{.i18n.create_dir}}
</li>
<li onclick="tree.removeIt();">
{{.i18n.delete}}
</li>
</ul>
</div>
<!-- 文件右键菜单 -->
<div id="fileRMenu" class="frame">
<ul>
<li onclick="tree.removeIt();">
{{.i18n.delete}}
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="edit-panel">
<div class="toolbars fn-none">
<span onclick="wide.run()" class="font-ico ico-buildrun" title="{{.i18n.build_n_run}}">
</span>
<span onclick="wide.saveFile()" title="{{.i18n.save}}" class="font-ico ico-save">
</span>
<span onclick="wide.fmt(editors.getCurrentPath(), wide.curEditor)" class="ico-format font-ico" title="{{.i18n.format}}">
</span>
<span class="font-ico ico-max" onclick="windows.maxEditor()" title="{{.i18n.max_editor}}">
</span>
</div>
<div class="tabs">
</div>
<div class="tabs-panel">
</div>
</div>
<div class="bottom-window-group">
<span title="{{.i18n.min}}" class="font-ico ico-min">
</span>
<div class="tabs">
<div class="current" data-index="output">
<span title="{{.i18n.output}}">
{{.i18n.output}}
</span>
</div>
<div data-index="search">
<span title="{{.i18n.search}}">
{{.i18n.search}}
</span>
</div>
<div data-index="notification">
<span title="{{.i18n.notification}}">
{{.i18n.notification}}
</span>
</div>
</div>
<div class="tabs-panel">
<div data-index="output">
<textarea class="output">
</textarea>
</div>
<div class="fn-none" data-index="search">
<div tabindex="-1" class="search">
<div class="tabs fn-none">
<div class="current" data-index="first">
<span class="first">
</span>
<span class="ico-close font-ico">
</span>
</div>
</div>
<div class="tabs-panel">
<div data-index="first">
</div>
</div>
</div>
</div>
<div class="fn-none" data-index="notification">
<div tabindex="-1" class="notification">
<table cellpadding="0" cellspacing="0">
</table>
</div>
</div>
</div>
</div>
</div>
<div class="footer fn-clear">
<div class="fn-left">
<span title="{{.i18n.restore_side}}" class="font-ico ico-restore fn-none">
</span>
<span title="{{.i18n.resotre_bottom}}" class="font-ico ico-restore fn-none">
</span>
</div>
<div class="fn-right">
<span class="cursor">
</span>
<span class="notification-count" title="{{.i18n.unread_notification}}">
{{.i18n.notification}}!
</span>
</div>
</div>
<div id="dialogRemoveConfirm" class="fn-none">
{{.i18n.isDelete}}
<b>
</b>
?
</div>
<div id="dialogAlert" class="fn-none">
</div>
<div id="dialogNewFilePrompt" class="dialog-prompt fn-none">
<input/>
</div>
<div id="dialogNewDirPrompt" class="dialog-prompt fn-none">
<input/>
</div>
<div id="dialogGoLinePrompt" class="dialog-prompt fn-none">
<input/>
</div>
<div id="dialogSearchForm" class="dialog-form fn-none">
<input placeholder="{{.i18n.keyword}}" />
<input placeholder="{{.i18n.file_format}}" />
</div>
<script>
var config = {
"pathSeparator": {{.pathSeparator}},
"latestSessionContent": {{.latestSessionContent}},
"label": {
"restore_editor": "{{.i18n.restore_editor}}",
"max_editor": "{{.i18n.max_editor}}",
"delete": "{{.i18n.delete}}",
"cancel": "{{.i18n.cancel}}",
"goto_line": "{{.i18n.goto_line}}",
"goto": "{{.i18n.goto}}",
"create": "{{.i18n.create}}",
"create_file": "{{.i18n.create_file}}",
"create_dir": "{{.i18n.create_dir}}",
"tip": "{{.i18n.tip}}",
"confirm": "{{.i18n.confirm}}",
"build_n_run": "{{.i18n.build_n_run}}",
"stop": "{{.i18n.stop}}",
"usages": "{{.i18n.find_usages}}",
"search_text": "{{.i18n.search_text}}",
"search": "{{.i18n.search}}",
"initialise": "{{.i18n.initialise}}",
"confirm_save": "{{.i18n.confirm_save}}"
},
"channel": {
"editor": '{{.conf.EditorChannel}}',
"shell": '{{.conf.ShellChannel}}',
"output": '{{.conf.OutputChannel}}',
"session": '{{.conf.SessionChannel}}'
},
"wideSessionId": '{{.session.Id}}'
};
// 发往 Wide 的所有 AJAX 请求需要使用该函数创建请求参数.
function newWideRequest() {
var ret = {
sid: config.wideSessionId
}
return ret;
}
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/jquery-2.1.1.min.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/reconnecting-websocket.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/ztree/jquery.ztree.all-3.5.min.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/codemirror.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/lint/lint.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/lint/json-lint.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/selection/active-line.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/overwrite/codemirror-4.5/addon/hint/show-hint.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/hint/anyword-hint.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/display/rulers.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/edit/closebrackets.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/edit/matchbrackets.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/edit/closetag.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/search/searchcursor.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/search/match-highlighter.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/fold/foldcode.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/fold/foldgutter.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/fold/brace-fold.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/fold/xml-fold.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/fold/markdown-fold.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/addon/fold/comment-fold.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/mode/go/go.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/mode/xml/xml.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/mode/htmlmixed/htmlmixed.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/mode/javascript/javascript.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/mode/markdown/markdown.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/mode/css/css.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/mode/shell/shell.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-4.5/mode/sql/sql.js">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/lint/json-lint.js?{{.conf.StaticResourceVersion}}">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/lint/go-lint.js?{{.conf.StaticResourceVersion}}">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/tabs.js?{{.conf.StaticResourceVersion}}">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/dialog.js?{{.conf.StaticResourceVersion}}">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/editors.js?{{.conf.StaticResourceVersion}}">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/notification.js?{{.conf.StaticResourceVersion}}">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/tree.js?{{.conf.StaticResourceVersion}}">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/wide.js?{{.conf.StaticResourceVersion}}">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/session.js?{{.conf.StaticResourceVersion}}">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/menu.js?{{.conf.StaticResourceVersion}}">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/windows.js?{{.conf.StaticResourceVersion}}">
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/hotkeys.js?{{.conf.StaticResourceVersion}}">
</script>
</body>
</html>

View File

@ -0,0 +1,16 @@
package main
import (
"fmt"
"mytest/time/pkg"
"time"
)
func main() {
for i := 0; i < 50; i++ {
fmt.Println("Hello, 世界", pkg.Now())
time.Sleep(time.Second)
}
}

View File

@ -0,0 +1,17 @@
package pkg
import (
"time"
)
func Now() time.Time {
return time.Now()
}
func Now1() time.Time {
return time.Now()
}
func Now2() {
}

1
data/workspace/README Executable file
View File

@ -0,0 +1 @@
Master workspace.

2
doc/README.md Normal file
View File

@ -0,0 +1,2 @@
* [User Guide](https://www.gitbook.io/book/88250/wide-user-guide)
* [Developer Guide](https://www.gitbook.io/book/88250/wide-dev-guide)

View File

@ -1,18 +1,4 @@
// Copyright (c) 2014-present, b3log.org // 编辑器操作.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package editor includes editor related manipulations.
package editor package editor
import ( import (
@ -21,88 +7,147 @@ import (
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"github.com/88250/gulu" "github.com/b3log/wide/conf"
"github.com/88250/wide/conf" "github.com/b3log/wide/file"
"github.com/88250/wide/file" "github.com/b3log/wide/session"
"github.com/88250/wide/session" "github.com/b3log/wide/util"
"github.com/golang/glog"
"github.com/gorilla/websocket"
) )
// Logger. var editorWS = map[string]*websocket.Conn{}
var logger = gulu.Log.NewLogger(os.Stdout)
// AutocompleteHandler handles request of code autocompletion. // 建立编辑器通道.
func AutocompleteHandler(w http.ResponseWriter, r *http.Request) { func WSHandler(w http.ResponseWriter, r *http.Request) {
if conf.Wide.ReadOnly { session, _ := session.HTTPSession.Get(r, "wide-session")
http.Error(w, "Forbidden", http.StatusForbidden) sid := session.Values["id"].(string)
return
editorWS[sid], _ = websocket.Upgrade(w, r, nil, 1024, 1024)
ret := map[string]interface{}{"output": "Editor initialized", "cmd": "init-editor"}
editorWS[sid].WriteJSON(&ret)
glog.Infof("Open a new [Editor] with session [%s], %d", sid, len(editorWS))
args := map[string]interface{}{}
for {
if err := editorWS[sid].ReadJSON(&args); err != nil {
if err.Error() == "EOF" {
return
}
if err.Error() == "unexpected EOF" {
return
}
glog.Error("Editor WS ERROR: " + err.Error())
return
}
code := args["code"].(string)
line := int(args["cursorLine"].(float64))
ch := int(args["cursorCh"].(float64))
offset := getCursorOffset(code, line, ch)
// glog.Infof("offset: %d", offset)
gocode := conf.Wide.GetGocode()
argv := []string{"-f=json", "autocomplete", strconv.Itoa(offset)}
var output bytes.Buffer
cmd := exec.Command(gocode, argv...)
cmd.Stdout = &output
stdin, _ := cmd.StdinPipe()
cmd.Start()
stdin.Write([]byte(code))
stdin.Close()
cmd.Wait()
ret = map[string]interface{}{"output": string(output.Bytes()), "cmd": "autocomplete"}
if err := editorWS[sid].WriteJSON(&ret); err != nil {
glog.Error("Editor WS ERROR: " + err.Error())
return
}
} }
}
// 自动完成(代码补全).
func AutocompleteHandler(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var args map[string]interface{} var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err) if err := decoder.Decode(&args); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) glog.Error(err)
http.Error(w, err.Error(), 500)
return return
} }
session, _ := session.HTTPSession.Get(r, session.CookieName) session, _ := session.HTTPSession.Get(r, "wide-session")
if session.IsNew { username := session.Values["username"].(string)
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
uid := session.Values["uid"].(string)
path := args["path"].(string) path := args["path"].(string)
fout, err := os.Create(path) fout, err := os.Create(path)
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), 500)
return return
} }
code := args["code"].(string) code := args["code"].(string)
fout.WriteString(code) fout.WriteString(code)
if err := fout.Close(); nil != err { if err := fout.Close(); nil != err {
logger.Error(err) glog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), 500)
return return
} }
line := int(args["cursorLine"].(float64)) line := int(args["cursorLine"].(float64))
ch := int(args["cursorCh"].(float64)) ch := int(args["cursorCh"].(float64))
offset := getCursorOffset(code, line, ch) offset := getCursorOffset(code, line, ch)
logger.Tracef("offset: %d", offset)
userWorkspace := conf.GetUserWorkspace(uid) // glog.Infof("offset: %d", offset)
workspaces := filepath.SplitList(userWorkspace)
libPath := ""
for _, workspace := range workspaces {
userLib := workspace + conf.PathSeparator + "pkg" + conf.PathSeparator +
runtime.GOOS + "_" + runtime.GOARCH
libPath += userLib + conf.PathListSeparator
}
logger.Tracef("gocode set lib-path [%s]", libPath) userWorkspace := conf.Wide.GetUserWorkspace(username)
gocode := gulu.Go.GetExecutableInGOBIN("gocode") //glog.Infof("User [%s] workspace [%s]", username, userWorkspace)
// FIXME: using gocode set lib-path has some issues while accrossing workspaces userLib := userWorkspace + conf.PathSeparator + "pkg" + conf.PathSeparator +
exec.Command(gocode, []string{"set", "lib-path", libPath}...).Run() runtime.GOOS + "_" + runtime.GOARCH
argv := []string{"-f=json", "--in=" + path, "autocomplete", strconv.Itoa(offset)} libPath := userLib
//glog.Infof("gocode set lib-path %s", libPath)
// FIXME: 使用 gocode set lib-path 在多工作空间环境下肯定是有问题的,需要考虑其他实现方式
gocode := conf.Wide.GetGocode()
argv := []string{"set", "lib-path", libPath}
exec.Command(gocode, argv...).Run()
argv = []string{"-f=json", "autocomplete", strconv.Itoa(offset)}
cmd := exec.Command(gocode, argv...) cmd := exec.Command(gocode, argv...)
stdin, _ := cmd.StdinPipe()
stdin.Write([]byte(code))
stdin.Close()
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), 500)
return return
} }
@ -111,31 +156,33 @@ func AutocompleteHandler(w http.ResponseWriter, r *http.Request) {
w.Write(output) w.Write(output)
} }
// GetExprInfoHandler handles request of getting expression infomation. // 查看表达式信息.
func GetExprInfoHandler(w http.ResponseWriter, r *http.Request) { func GetExprInfoHandler(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult() data := map[string]interface{}{"succ": true}
defer gulu.Ret.RetResult(w, r, result) defer util.RetJSON(w, r, data)
session, _ := session.HTTPSession.Get(r, session.CookieName) session, _ := session.HTTPSession.Get(r, "wide-session")
uid := session.Values["uid"].(string) username := session.Values["username"].(string)
decoder := json.NewDecoder(r.Body)
var args map[string]interface{} var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil { if err := decoder.Decode(&args); err != nil {
logger.Error(err) glog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), 500)
return return
} }
path := args["path"].(string) path := args["path"].(string)
curDir := filepath.Dir(path) curDir := path[:strings.LastIndex(path, conf.PathSeparator)]
filename := filepath.Base(path) filename := path[strings.LastIndex(path, conf.PathSeparator)+1:]
fout, err := os.Create(path) fout, err := os.Create(path)
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
result.Code = -1 data["succ"] = false
return return
} }
@ -144,8 +191,8 @@ func GetExprInfoHandler(w http.ResponseWriter, r *http.Request) {
fout.WriteString(code) fout.WriteString(code)
if err := fout.Close(); nil != err { if err := fout.Close(); nil != err {
logger.Error(err) glog.Error(err)
result.Code = -1 data["succ"] = false
return return
} }
@ -155,63 +202,61 @@ func GetExprInfoHandler(w http.ResponseWriter, r *http.Request) {
offset := getCursorOffset(code, line, ch) offset := getCursorOffset(code, line, ch)
logger.Tracef("offset [%d]", offset) // glog.Infof("offset [%d]", offset)
ideStub := gulu.Go.GetExecutableInGOBIN("gotools") // TODO: 目前是调用 liteide_stub 工具来查找声明,后续需要重新实现
argv := []string{"types", "-pos", filename + ":" + strconv.Itoa(offset), "-info", "."} ide_stub := conf.Wide.GetIDEStub()
cmd := exec.Command(ideStub, argv...) argv := []string{"type", "-cursor", filename + ":" + strconv.Itoa(offset), "-info", "."}
cmd := exec.Command(ide_stub, argv...)
cmd.Dir = curDir cmd.Dir = curDir
setCmdEnv(cmd, uid) setCmdEnv(cmd, username)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), 500)
return return
} }
exprInfo := strings.TrimSpace(string(output)) exprInfo := strings.TrimSpace(string(output))
if "" == exprInfo { if "" == exprInfo {
result.Code = -1 data["succ"] = false
return return
} }
result.Data = exprInfo data["info"] = exprInfo
} }
// FindDeclarationHandler handles request of finding declaration. // 查找声明.
func FindDeclarationHandler(w http.ResponseWriter, r *http.Request) { func FindDeclarationHandler(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult() data := map[string]interface{}{"succ": true}
defer gulu.Ret.RetResult(w, r, result) defer util.RetJSON(w, r, data)
session, _ := session.HTTPSession.Get(r, session.CookieName) session, _ := session.HTTPSession.Get(r, "wide-session")
if session.IsNew { username := session.Values["username"].(string)
http.Error(w, "Forbidden", http.StatusForbidden)
return decoder := json.NewDecoder(r.Body)
}
uid := session.Values["uid"].(string)
var args map[string]interface{} var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil { if err := decoder.Decode(&args); err != nil {
logger.Error(err) glog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), 500)
return return
} }
path := args["path"].(string) path := args["path"].(string)
curDir := filepath.Dir(path) curDir := path[:strings.LastIndex(path, conf.PathSeparator)]
filename := filepath.Base(path) filename := path[strings.LastIndex(path, conf.PathSeparator)+1:]
fout, err := os.Create(path) fout, err := os.Create(path)
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
result.Code = -1 data["succ"] = false
return return
} }
@ -220,8 +265,8 @@ func FindDeclarationHandler(w http.ResponseWriter, r *http.Request) {
fout.WriteString(code) fout.WriteString(code)
if err := fout.Close(); nil != err { if err := fout.Close(); nil != err {
logger.Error(err) glog.Error(err)
result.Code = -1 data["succ"] = false
return return
} }
@ -231,26 +276,27 @@ func FindDeclarationHandler(w http.ResponseWriter, r *http.Request) {
offset := getCursorOffset(code, line, ch) offset := getCursorOffset(code, line, ch)
logger.Tracef("offset [%d]", offset) // glog.Infof("offset [%d]", offset)
ideStub := gulu.Go.GetExecutableInGOBIN("gotools") // TODO: 目前是调用 liteide_stub 工具来查找声明,后续需要重新实现
argv := []string{"types", "-pos", filename + ":" + strconv.Itoa(offset), "-def", "."} ide_stub := conf.Wide.GetIDEStub()
cmd := exec.Command(ideStub, argv...) argv := []string{"type", "-cursor", filename + ":" + strconv.Itoa(offset), "-def", "."}
cmd := exec.Command(ide_stub, argv...)
cmd.Dir = curDir cmd.Dir = curDir
setCmdEnv(cmd, uid) setCmdEnv(cmd, username)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), 500)
return return
} }
found := strings.TrimSpace(string(output)) found := strings.TrimSpace(string(output))
if "" == found { if "" == found {
result.Code = -1 data["succ"] = false
return return
} }
@ -258,49 +304,42 @@ func FindDeclarationHandler(w http.ResponseWriter, r *http.Request) {
part := found[:strings.LastIndex(found, ":")] part := found[:strings.LastIndex(found, ":")]
cursorSep := strings.LastIndex(part, ":") cursorSep := strings.LastIndex(part, ":")
path = found[:cursorSep] path = found[:cursorSep]
cursorLine := found[cursorSep+1 : strings.LastIndex(found, ":")]
cursorCh := found[strings.LastIndex(found, ":")+1:]
cursorLine, _ := strconv.Atoi(found[cursorSep+1 : strings.LastIndex(found, ":")]) data["path"] = path
cursorCh, _ := strconv.Atoi(found[strings.LastIndex(found, ":")+1:])
data := map[string]interface{}{}
result.Data = &data
data["path"] = filepath.ToSlash(path)
data["cursorLine"] = cursorLine data["cursorLine"] = cursorLine
data["cursorCh"] = cursorCh data["cursorCh"] = cursorCh
} }
// FindUsagesHandler handles request of finding usages. // 查找使用.
func FindUsagesHandler(w http.ResponseWriter, r *http.Request) { func FindUsagesHandler(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult() data := map[string]interface{}{"succ": true}
defer gulu.Ret.RetResult(w, r, result) defer util.RetJSON(w, r, data)
session, _ := session.HTTPSession.Get(r, session.CookieName) session, _ := session.HTTPSession.Get(r, "wide-session")
if session.IsNew { username := session.Values["username"].(string)
http.Error(w, "Forbidden", http.StatusForbidden)
return decoder := json.NewDecoder(r.Body)
}
uid := session.Values["uid"].(string)
var args map[string]interface{} var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil { if err := decoder.Decode(&args); err != nil {
logger.Error(err) glog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), 500)
return return
} }
filePath := args["path"].(string) filePath := args["path"].(string)
curDir := filepath.Dir(filePath) curDir := filePath[:strings.LastIndex(filePath, conf.PathSeparator)]
filename := filepath.Base(filePath) filename := filePath[strings.LastIndex(filePath, conf.PathSeparator)+1:]
fout, err := os.Create(filePath) fout, err := os.Create(filePath)
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
result.Code = -1 data["succ"] = false
return return
} }
@ -309,8 +348,8 @@ func FindUsagesHandler(w http.ResponseWriter, r *http.Request) {
fout.WriteString(code) fout.WriteString(code)
if err := fout.Close(); nil != err { if err := fout.Close(); nil != err {
logger.Error(err) glog.Error(err)
result.Code = -1 data["succ"] = false
return return
} }
@ -319,38 +358,39 @@ func FindUsagesHandler(w http.ResponseWriter, r *http.Request) {
ch := int(args["cursorCh"].(float64)) ch := int(args["cursorCh"].(float64))
offset := getCursorOffset(code, line, ch) offset := getCursorOffset(code, line, ch)
logger.Tracef("offset [%d]", offset) // glog.Infof("offset [%d]", offset)
ideStub := gulu.Go.GetExecutableInGOBIN("gotools") // TODO: 目前是调用 liteide_stub 工具来查找使用,后续需要重新实现
argv := []string{"types", "-pos", filename + ":" + strconv.Itoa(offset), "-use", "."} ide_stub := conf.Wide.GetIDEStub()
cmd := exec.Command(ideStub, argv...) argv := []string{"type", "-cursor", filename + ":" + strconv.Itoa(offset), "-use", "."}
cmd := exec.Command(ide_stub, argv...)
cmd.Dir = curDir cmd.Dir = curDir
setCmdEnv(cmd, uid) setCmdEnv(cmd, username)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), 500)
return return
} }
out := strings.TrimSpace(string(output)) result := strings.TrimSpace(string(output))
if "" == out { if "" == result {
result.Code = -1 data["succ"] = false
return return
} }
founds := strings.Split(out, "\n") founds := strings.Split(result, "\n")
usages := []*file.Snippet{} usages := []*file.Snippet{}
for _, found := range founds { for _, found := range founds {
found = strings.TrimSpace(found) found = strings.TrimSpace(found)
part := found[:strings.LastIndex(found, ":")] part := found[:strings.LastIndex(found, ":")]
cursorSep := strings.LastIndex(part, ":") cursorSep := strings.LastIndex(part, ":")
path := filepath.ToSlash(found[:cursorSep]) path := found[:cursorSep]
cursorLine, _ := strconv.Atoi(found[cursorSep+1 : strings.LastIndex(found, ":")]) cursorLine, _ := strconv.Atoi(found[cursorSep+1 : strings.LastIndex(found, ":")])
cursorCh, _ := strconv.Atoi(found[strings.LastIndex(found, ":")+1:]) cursorCh, _ := strconv.Atoi(found[strings.LastIndex(found, ":")+1:])
@ -358,22 +398,21 @@ func FindUsagesHandler(w http.ResponseWriter, r *http.Request) {
usages = append(usages, usage) usages = append(usages, usage)
} }
result.Data = usages data["founds"] = usages
} }
// getCursorOffset calculates the cursor offset. // 计算光标偏移位置.
// //
// line is the line number, starts with 0 that means the first line // line 指定了行号(第一行为 0ch 指定了列号(第一列为 0.
// ch is the column number, starts with 0 that means the first column
func getCursorOffset(code string, line, ch int) (offset int) { func getCursorOffset(code string, line, ch int) (offset int) {
lines := strings.Split(code, "\n") lines := strings.Split(code, "\n")
// calculate sum length of lines before // 计算前几行长度
for i := 0; i < line; i++ { for i := 0; i < line; i++ {
offset += len(lines[i]) offset += len(lines[i])
} }
// calculate length of the current line and column // 计算当前行、当前列长度
curLine := lines[line] curLine := lines[line]
var buffer bytes.Buffer var buffer bytes.Buffer
r := []rune(curLine) r := []rune(curLine)
@ -381,14 +420,14 @@ func getCursorOffset(code string, line, ch int) (offset int) {
buffer.WriteString(string(r[i])) buffer.WriteString(string(r[i]))
} }
offset += len(buffer.String()) // append length of current line offset += line // 加换行符
offset += line // append number of '\n' offset += len(buffer.String()) // 加当前行列偏移
return offset return offset
} }
func setCmdEnv(cmd *exec.Cmd, userId string) { func setCmdEnv(cmd *exec.Cmd, username string) {
userWorkspace := conf.GetUserWorkspace(userId) userWorkspace := conf.Wide.GetUserWorkspace(username)
cmd.Env = append(cmd.Env, cmd.Env = append(cmd.Env,
"GOPATH="+userWorkspace, "GOPATH="+userWorkspace,

View File

@ -1,17 +1,3 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package editor package editor
import ( import (
@ -20,56 +6,36 @@ import (
"os" "os"
"os/exec" "os/exec"
"github.com/88250/gulu" "github.com/88250/gohtml"
"github.com/88250/wide/conf" "github.com/b3log/wide/util"
"github.com/88250/wide/session" "github.com/golang/glog"
) )
// GoFmtHandler handles request of formatting Go source code. // TODO: 加入 goimports 格式化 Go 源码文件
//
// This function will select a format tooll based on user's configuration: // gofmt 格式化 Go 源码文件.
// 1. gofmt
// 2. goimports
func GoFmtHandler(w http.ResponseWriter, r *http.Request) { func GoFmtHandler(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult() data := map[string]interface{}{"succ": true}
defer gulu.Ret.RetResult(w, r, result) defer util.RetJSON(w, r, data)
if conf.Wide.ReadOnly { decoder := json.NewDecoder(r.Body)
result.Code = -1
result.Msg = "readonly mode"
return
}
session, _ := session.HTTPSession.Get(r, session.CookieName)
if session.IsNew {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
uid := session.Values["uid"].(string)
var args map[string]interface{} var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil { if err := decoder.Decode(&args); err != nil {
logger.Error(err) glog.Error(err)
result.Code = -1 data["succ"] = false
return return
} }
filePath := args["file"].(string) filePath := args["file"].(string)
if gulu.Go.IsAPI(filePath) {
result.Code = -1
return
}
fout, err := os.Create(filePath) fout, err := os.Create(filePath)
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
result.Code = -1 data["succ"] = false
return return
} }
@ -78,29 +44,19 @@ func GoFmtHandler(w http.ResponseWriter, r *http.Request) {
fout.WriteString(code) fout.WriteString(code)
if err := fout.Close(); nil != err { if err := fout.Close(); nil != err {
logger.Error(err) glog.Error(err)
result.Code = -1 data["succ"] = false
return return
} }
data := map[string]interface{}{}
result.Data = &data
data["code"] = code
result.Data = data
fmt := conf.GetGoFmt(uid)
argv := []string{filePath} argv := []string{filePath}
cmd := exec.Command(fmt, argv...) cmd := exec.Command("gofmt", argv...)
bytes, _ := cmd.Output() bytes, _ := cmd.Output()
output := string(bytes) output := string(bytes)
if "" == output { if "" == output {
// format error, returns the original content data["succ"] = false
result.Code = 0
return return
} }
@ -111,8 +67,133 @@ func GoFmtHandler(w http.ResponseWriter, r *http.Request) {
fout, err = os.Create(filePath) fout, err = os.Create(filePath)
fout.WriteString(code) fout.WriteString(code)
if err := fout.Close(); nil != err { if err := fout.Close(); nil != err {
logger.Error(err) glog.Error(err)
result.Code = -1 data["succ"] = false
return
}
}
// 格式化 HTML 文件.
// FIXME依赖的工具 gohtml 格式化 HTML 时有问题
func HTMLFmtHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{"succ": true}
defer util.RetJSON(w, r, data)
decoder := json.NewDecoder(r.Body)
var args map[string]interface{}
if err := decoder.Decode(&args); err != nil {
glog.Error(err)
data["succ"] = false
return
}
filePath := args["file"].(string)
fout, err := os.Create(filePath)
if nil != err {
glog.Error(err)
data["succ"] = false
return
}
code := args["code"].(string)
fout.WriteString(code)
if err := fout.Close(); nil != err {
glog.Error(err)
data["succ"] = false
return
}
output := gohtml.Format(code)
if "" == output {
data["succ"] = false
return
}
code = string(output)
data["code"] = code
fout, err = os.Create(filePath)
fout.WriteString(code)
if err := fout.Close(); nil != err {
glog.Error(err)
data["succ"] = false
return
}
}
// 格式化 JSON 文件.
func JSONFmtHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{"succ": true}
defer util.RetJSON(w, r, data)
decoder := json.NewDecoder(r.Body)
var args map[string]interface{}
if err := decoder.Decode(&args); err != nil {
glog.Error(err)
data["succ"] = false
return
}
filePath := args["file"].(string)
fout, err := os.Create(filePath)
if nil != err {
glog.Error(err)
data["succ"] = false
return
}
code := args["code"].(string)
fout.WriteString(code)
if err := fout.Close(); nil != err {
glog.Error(err)
data["succ"] = false
return
}
obj := new(interface{})
if err := json.Unmarshal([]byte(code), &obj); nil != err {
glog.Error(err)
data["succ"] = false
return
}
glog.Info(obj)
bytes, err := json.MarshalIndent(obj, "", " ")
if nil != err {
data["succ"] = false
return
}
code = string(bytes)
data["code"] = code
fout, err = os.Create(filePath)
fout.WriteString(code)
if err := fout.Close(); nil != err {
glog.Error(err)
data["succ"] = false
return return
} }

View File

@ -1,119 +1,89 @@
// Copyright (c) 2014-present, b3log.org // 事件处理.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package event includes event related manipulations.
package event package event
import ( import "github.com/golang/glog"
"os"
"github.com/88250/gulu"
)
const ( const (
// EvtCodeGOPATHNotFound indicates an event: not found $GOPATH env variable EvtCodeGOPATHNotFound = iota // 事件代码:找不到环境变量 $GOPATH
EvtCodeGOPATHNotFound = iota EvtCodeGOROOTNotFound // 事件代码:找不到环境变量 $GOROOT
// EvtCodeGOROOTNotFound indicates an event: not found $GOROOT env variable EvtCodeGocodeNotFound // 事件代码:找不到 gocode
EvtCodeGOROOTNotFound EvtCodeIDEStubNotFound // 事件代码:找不到 IDE stub
// EvtCodeGocodeNotFound indicates an event: not found gocode
EvtCodeGocodeNotFound
// EvtCodeIDEStubNotFound indicates an event: not found gotools
EvtCodeIDEStubNotFound
// EvtCodeServerInternalError indicates an event: server internal error
EvtCodeServerInternalError
) )
// Max length of queue. // 事件队列最大长度.
const maxQueueLength = 10 const MaxQueueLength = 10
// Logger. // 事件结构.
var logger = gulu.Log.NewLogger(os.Stdout)
// Event represents an event.
type Event struct { type Event struct {
Code int `json:"code"` // event code Code int `json:"code"` // 事件代码
Sid string `json:"sid"` // wide session id related Sid string `json:"sid"` // 用户会话 id
Data interface{} `json:"data"` // event data
} }
// Global event queue. // 全局事件队列.
// //
// Every event in this queue will be dispatched to each user event queue. // 入队的事件将分发到每个用户的事件队列中.
var EventQueue = make(chan *Event, maxQueueLength) var EventQueue = make(chan int, MaxQueueLength)
// UserEventQueue represents a user event queue. // 用户事件队列.
type UserEventQueue struct { type UserEventQueue struct {
Sid string // wide session id related Sid string // 关联的会话 id
Queue chan *Event // queue Queue chan int // 队列
Handlers []Handler // event handlers Handlers []Handler // 事件处理器集
} }
type queues map[string]*UserEventQueue // 事件队列集类型.
type Queues map[string]*UserEventQueue
// User event queues. // 用户事件队列集.
// //
// <sid, *UserEventQueue> // <sid, *UserEventQueue>
var UserEventQueues = queues{} var UserEventQueues = Queues{}
// Load initializes the event handling. // 加载事件处理.
func Load() { func Load() {
go func() { go func() {
defer gulu.Panic.Recover(nil)
for event := range EventQueue { for event := range EventQueue {
logger.Debugf("Received a global event [code=%d]", event.Code) glog.V(5).Info("收到全局事件 [%d]", event)
// dispatch the event to each user event queue // 将事件分发到每个用户的事件队列里
for _, userQueue := range UserEventQueues { for _, userQueue := range UserEventQueues {
event.Sid = userQueue.Sid
userQueue.Queue <- event userQueue.Queue <- event
} }
} }
}() }()
} }
// AddHandler adds the specified handlers to user event queues. // 为用户队列添加事件处理器.
func (uq *UserEventQueue) AddHandler(handlers ...Handler) { func (uq *UserEventQueue) AddHandler(handlers ...Handler) {
uq.Handlers = append(uq.Handlers, handlers...) for _, handler := range handlers {
uq.Handlers = append(uq.Handlers, handler)
}
} }
// New initializes a user event queue with the specified wide session id. // 初始化一个用户事件队列.
func (ueqs queues) New(sid string) *UserEventQueue { func (ueqs Queues) New(sid string) *UserEventQueue {
q := ueqs[sid]
if q, ok := ueqs[sid]; ok { if nil != q {
logger.Warnf("Already exist a user queue in session [%s]", sid) glog.Warningf("Already exist a user queue in session [%s]", sid)
return q return q
} }
q := &UserEventQueue{ q = &UserEventQueue{
Sid: sid, Sid: sid,
Queue: make(chan *Event, maxQueueLength), Queue: make(chan int, MaxQueueLength),
} }
ueqs[sid] = q ueqs[sid] = q
go func() { // start listening go func() { // 队列开始监听事件
defer gulu.Panic.Recover(nil) for evtCode := range q.Queue {
glog.V(5).Infof("Session [%s] received a event [%d]", sid, evtCode)
for evt := range q.Queue { // 将事件交给事件处理器进行处理
logger.Debugf("Session [%s] received an event [%d]", sid, evt.Code)
// process event by each handlers
for _, handler := range q.Handlers { for _, handler := range q.Handlers {
handler.Handle(evt) handler.Handle(&Event{Code: evtCode, Sid: sid})
} }
} }
}() }()
@ -121,24 +91,25 @@ func (ueqs queues) New(sid string) *UserEventQueue {
return q return q
} }
// Close closes a user event queue with the specified wide session id. // 关闭一个用户事件队列.
func (ueqs queues) Close(sid string) { func (ueqs Queues) Close(sid string) {
q := ueqs[sid]
if q, ok := ueqs[sid]; ok { if nil == q {
close(q.Queue) return
delete(ueqs, sid)
} }
delete(ueqs, sid)
} }
// Handler represents an event handler. // 事件处理接口.
type Handler interface { type Handler interface {
Handle(event *Event) Handle(event *Event)
} }
// HandleFunc represents a handler function. // 函数指针包装.
type HandleFunc func(event *Event) type HandleFunc func(event *Event)
// Default implementation of event handling. // 事件处理默认实现.
func (fn HandleFunc) Handle(event *Event) { func (fn HandleFunc) Handle(event *Event) {
fn(event) fn(event)
} }

View File

@ -1,102 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"github.com/88250/gulu"
)
// GetZipHandler handles request of retrieving zip file.
func GetZipHandler(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
path := q["path"][0]
if ".zip" != filepath.Ext(path) {
http.Error(w, "Bad Request", 400)
return
}
if !gulu.File.IsExist(path) {
http.Error(w, "Not Found", 404)
return
}
filename := filepath.Base(path)
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
w.Header().Set("Content-Type", "application/zip")
http.ServeFile(w, r, path)
os.Remove(path)
}
// CreateZipHandler handles request of creating zip.
func CreateZipHandler(w http.ResponseWriter, r *http.Request) {
data := gulu.Ret.NewResult()
defer gulu.Ret.RetResult(w, r, data)
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
data.Code = -1
return
}
path := args["path"].(string)
var name string
base := filepath.Base(path)
if nil != args["name"] {
name = args["name"].(string)
} else {
name = base
}
dir := filepath.Dir(path)
if !gulu.File.IsExist(path) {
data.Code = -1
data.Msg = "Can't find file [" + path + "]"
return
}
zipPath := filepath.Join(dir, name)
zipFile, err := gulu.Zip.Create(zipPath + ".zip")
if nil != err {
logger.Error(err)
data.Code = -1
return
}
defer zipFile.Close()
if gulu.File.IsDir(path) {
zipFile.AddDirectory(base, path)
} else {
zipFile.AddEntry(base, path)
}
data.Data = zipPath
}

File diff suppressed because it is too large Load Diff

View File

@ -1,159 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
import (
"bytes"
"encoding/json"
"go/ast"
"go/parser"
"go/token"
"net/http"
"strings"
"github.com/88250/gulu"
)
type element struct {
Name string
Line int
Ch int
}
// GetOutlineHandler gets outfile of a go file.
func GetOutlineHandler(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult()
defer gulu.Ret.RetResult(w, r, result)
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
result.Code = -1
return
}
code := args["code"].(string)
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", code, 0)
if err != nil {
result.Code = -1
return
}
data := map[string]interface{}{}
result.Data = &data
// ast.Print(fset, f)
line, ch := getCursor(code, int(f.Name.Pos()))
data["package"] = &element{Name: f.Name.Name, Line: line, Ch: ch}
imports := []*element{}
for _, astImport := range f.Imports {
line, ch := getCursor(code, int(astImport.Path.Pos()))
imports = append(imports, &element{Name: astImport.Path.Value, Line: line, Ch: ch})
}
data["imports"] = imports
funcDecls := []*element{}
varDecls := []*element{}
constDecls := []*element{}
structDecls := []*element{}
interfaceDecls := []*element{}
typeDecls := []*element{}
for _, decl := range f.Decls {
switch decl.(type) {
case *ast.FuncDecl:
funcDecl := decl.(*ast.FuncDecl)
line, ch := getCursor(code, int(funcDecl.Name.Pos()))
funcDecls = append(funcDecls, &element{Name: funcDecl.Name.Name, Line: line, Ch: ch})
case *ast.GenDecl:
genDecl := decl.(*ast.GenDecl)
for _, spec := range genDecl.Specs {
switch genDecl.Tok {
case token.VAR:
variableSpec := spec.(*ast.ValueSpec)
for _, varName := range variableSpec.Names {
line, ch := getCursor(code, int(varName.Pos()))
varDecls = append(varDecls, &element{Name: varName.Name, Line: line, Ch: ch})
}
case token.TYPE:
typeSpec := spec.(*ast.TypeSpec)
line, ch := getCursor(code, int(typeSpec.Pos()))
switch typeSpec.Type.(type) {
case *ast.StructType:
structDecls = append(structDecls, &element{Name: typeSpec.Name.Name, Line: line, Ch: ch})
case *ast.InterfaceType:
interfaceDecls = append(interfaceDecls, &element{Name: typeSpec.Name.Name, Line: line, Ch: ch})
case *ast.Ident:
typeDecls = append(typeDecls, &element{Name: typeSpec.Name.Name, Line: line, Ch: ch})
}
case token.CONST:
constSpec := spec.(*ast.ValueSpec)
for _, constName := range constSpec.Names {
line, ch := getCursor(code, int(constName.Pos()))
constDecls = append(constDecls, &element{Name: constName.Name, Line: line, Ch: ch})
}
}
}
}
}
data["funcDecls"] = funcDecls
data["varDecls"] = varDecls
data["constDecls"] = constDecls
data["structDecls"] = structDecls
data["interfaceDecls"] = interfaceDecls
data["typeDecls"] = typeDecls
}
// getCursor calculates the cursor position (line, ch) by the specified offset.
//
// line is the line number, starts with 0 that means the first line
// ch is the column number, starts with 0 that means the first column
func getCursor(code string, offset int) (line, ch int) {
code = code[:offset]
lines := strings.Split(code, "\n")
line = 0
for range lines {
line++
}
var buffer bytes.Buffer
runes := []rune(lines[line-1])
for _, r := range runes {
buffer.WriteString(string(r))
}
ch = len(buffer.String())
return line - 1, ch - 1
}

23
go.mod
View File

@ -1,23 +0,0 @@
module github.com/88250/wide
go 1.20
require (
github.com/88250/gulu v1.1.0
github.com/fsnotify/fsnotify v1.4.9
github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.4.2
github.com/parnurzeal/gorequest v0.2.16
)
require (
github.com/davidebianchi/go-jsonclient v1.5.0 // indirect
github.com/elazarl/goproxy v0.0.0-20200426045556-49ad98f6dac1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
golang.org/x/sys v0.0.0-20200408040146-ea54a3c99b9b // indirect
golang.org/x/text v0.3.2 // indirect
moul.io/http2curl v1.0.0 // indirect
)

44
go.sum
View File

@ -1,44 +0,0 @@
github.com/88250/gulu v1.1.0 h1:aU8HncW1XNssT1insCOz6rvmTUDrtsDwSnzeqTEqNFA=
github.com/88250/gulu v1.1.0/go.mod h1:a2POIziN+QFeNT4Mj7FHH2lz1HEaFlMRF6wPE0NHM4U=
github.com/davidebianchi/go-jsonclient v1.5.0 h1:PVDunAF/6c30D2SSx711efsrUP3hhml+uGT6oD3lzVY=
github.com/davidebianchi/go-jsonclient v1.5.0/go.mod h1:lXd/hkx23H590Dod74j5GsmpgxF8RIAGZDt1P5+2mqo=
github.com/elazarl/goproxy v0.0.0-20200426045556-49ad98f6dac1 h1:TEmChtx8+IeOghiySC8kQIr0JZOdKUmRmmkuRDuYs3E=
github.com/elazarl/goproxy v0.0.0-20200426045556-49ad98f6dac1/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/parnurzeal/gorequest v0.2.16 h1:T/5x+/4BT+nj+3eSknXmCTnEVGSzFzPGdpqmUVVZXHQ=
github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200408040146-ea54a3c99b9b h1:h03Ur1RlPrGTjua4koYdpGl8W0eYo8p1uI9w7RPlkdk=
golang.org/x/sys v0.0.0-20200408040146-ea54a3c99b9b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=

View File

@ -1,150 +0,0 @@
/*
* Copyright (c) 2014-2018, b3log.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @file frontend tool.
*
* @author <a href="mailto:liliyuan@fangstar.net">Liyuan Li</a>
* @version 0.2.0.0, Oct 5, 2018
*/
var gulp = require('gulp')
var concat = require('gulp-concat')
var cleanCSS = require('gulp-clean-css')
var uglify = require('gulp-uglify')
var sourcemaps = require('gulp-sourcemaps')
function minLibCSS () {
// css
var cssLibs = [
'./static/js/lib/jquery-layout/layout-default-latest.css',
'./static/js/lib/codemirror-5.1/codemirror.css',
'./static/js/lib/codemirror-5.1/addon/hint/show-hint.css',
'./static/js/lib/codemirror-5.1/addon/lint/lint.css',
'./static/js/lib/codemirror-5.1/addon/fold/foldgutter.css',
'./static/js/lib/codemirror-5.1/addon/dialog/dialog.css',
'./static/js/overwrite/codemirror/theme/*.css']
return gulp.src(cssLibs).
pipe(cleanCSS()).
pipe(concat('lib.min.css')).
pipe(gulp.dest('./static/css/'))
}
function minZTreeStyleCSS () {
return gulp.src('./static/js/lib/ztree/zTreeStyle.css').
pipe(cleanCSS()).
pipe(concat('zTreeStyle.min.css')).
pipe(gulp.dest('./static/js/lib/ztree/'))
}
function minWideCSS () {
var cssWide = [
'./static/css/dialog.css',
'./static/css/base.css',
'./static/css/wide.css',
'./static/css/side.css',
'./static/css/start.css',
'./static/css/about.css',
]
return gulp.src(cssWide).
pipe(cleanCSS()).
pipe(concat('wide.min.css')).
pipe(gulp.dest('./static/css/'))
}
function minLibJS () {
// js
var jsLibs = [
'./static/js/lib/jquery-2.1.1.min.js',
'./static/js/lib/jquery-ui.min.js',
'./static/js/lib/jquery-layout/jquery.layout-latest.js',
'./static/js/lib/reconnecting-websocket.js',
'./static/js/lib/Autolinker.min.js',
'./static/js/lib/emmet.js',
'./static/js/lib/js-beautify-1.5.4/beautify.js',
'./static/js/lib/js-beautify-1.5.4/beautify-html.js',
'./static/js/lib/js-beautify-1.5.4/beautify-css.js',
'./static/js/lib/jquery-file-upload-9.8.0/vendor/jquery.ui.widget.js',
'./static/js/lib/jquery-file-upload-9.8.0/jquery.iframe-transport.js',
'./static/js/lib/jquery-file-upload-9.8.0/jquery.fileupload.js',
'./static/js/lib/codemirror-5.1/codemirror.min.js',
'./static/js/lib/codemirror-5.1/addon/lint/lint.js',
'./static/js/lib/codemirror-5.1/addon/lint/json-lint.js',
'./static/js/lib/codemirror-5.1/addon/selection/active-line.js',
'./static/js/lib/codemirror-5.1/addon/selection/active-line.js',
'./static/js/overwrite/codemirror/addon/hint/show-hint.js',
'./static/js/lib/codemirror-5.1/addon/hint/anyword-hint.js',
'./static/js/lib/codemirror-5.1/addon/display/rulers.js',
'./static/js/lib/codemirror-5.1/addon/edit/closebrackets.js',
'./static/js/lib/codemirror-5.1/addon/edit/matchbrackets.js',
'./static/js/lib/codemirror-5.1/addon/edit/closetag.js',
'./static/js/lib/codemirror-5.1/addon/search/searchcursor.js',
'./static/js/lib/codemirror-5.1/addon/search/search.js',
'./static/js/lib/codemirror-5.1/addon/dialog/dialog.js',
'./static/js/lib/codemirror-5.1/addon/search/match-highlighter.js',
'./static/js/lib/codemirror-5.1/addon/fold/foldcode.js',
'./static/js/lib/codemirror-5.1/addon/fold/foldgutter.js',
'./static/js/lib/codemirror-5.1/addon/fold/brace-fold.js',
'./static/js/lib/codemirror-5.1/addon/fold/xml-fold.js',
'./static/js/lib/codemirror-5.1/addon/fold/markdown-fold.js',
'./static/js/lib/codemirror-5.1/addon/fold/comment-fold.js',
'./static/js/lib/codemirror-5.1/addon/mode/loadmode.js',
'./static/js/lib/codemirror-5.1/addon/comment/comment.js',
'./static/js/lib/codemirror-5.1/mode/meta.js',
'./static/js/lib/codemirror-5.1/mode/go/go.js',
'./static/js/lib/codemirror-5.1/mode/clike/clike.js',
'./static/js/lib/codemirror-5.1/mode/xml/xml.js',
'./static/js/lib/codemirror-5.1/mode/htmlmixed/htmlmixed.js',
'./static/js/lib/codemirror-5.1/mode/javascript/javascript.js',
'./static/js/lib/codemirror-5.1/mode/markdown/markdown.js',
'./static/js/lib/codemirror-5.1/mode/css/css.js',
'./static/js/lib/codemirror-5.1/mode/shell/shell.js',
'./static/js/lib/codemirror-5.1/mode/sql/sql.js',
'./static/js/lib/codemirror-5.1/keymap/vim.js',
'./static/js/lib/lint/json-lint.js',
'./static/js/lib/lint/go-lint.js']
return gulp.src(jsLibs).
pipe(uglify()).
pipe(concat('lib.min.js')).
pipe(gulp.dest('./static/js/'))
}
function minWideJS () {
var jsWide = [
'./static/js/tabs.js',
'./static/js/tabs.js',
'./static/js/dialog.js',
'./static/js/editors.js',
'./static/js/notification.js',
'./static/js/tree.js',
'./static/js/wide.js',
'./static/js/session.js',
'./static/js/menu.js',
'./static/js/windows.js',
'./static/js/hotkeys.js',
'./static/js/bottomGroup.js',
]
return gulp.src(jsWide).
pipe(sourcemaps.init()).
pipe(uglify()).
pipe(concat('wide.min.js')).
pipe(sourcemaps.write('.')).
pipe(gulp.dest('./static/js/'))
}
gulp.task('default',
gulp.series(
gulp.parallel(minLibCSS, minZTreeStyleCSS, minWideCSS, minLibJS, minWideJS)))

View File

@ -1,20 +1,18 @@
{ {
"colon": ": ",
"wide": "Wide", "wide": "Wide",
"wide_title": "Playing golang, anytime, anywhere", "isDelete": "Delete",
"cancel": "Cancel", "cancel": "Cancel",
"file": "File", "file": "File",
"login": "Login", "login": "Login",
"username": "Username", "username": "Username",
"current_user": "Current User", "current_user": "Current User",
"current_session": "Current Session",
"password": "Password", "password": "Password",
"login_error": "Login Error", "login_failed": "Login Failed",
"run": "Run", "run": "Run",
"debug": "Debug", "debug": "Debug",
"help": "Help", "help": "Help",
"check_update": "Check Update", "check_update": "Check Update",
"issues": "Issues", "report_issues": "Report Issues",
"wide_doc": "Wide Document", "wide_doc": "Wide Document",
"about": "About", "about": "About",
"start_page": "Start Page", "start_page": "Start Page",
@ -22,12 +20,12 @@
"create": "Create", "create": "Create",
"create_dir": "Create Dir", "create_dir": "Create Dir",
"delete": "Delete", "delete": "Delete",
"rename": "Rename",
"save": "Save", "save": "Save",
"exit": "Exit", "exit": "Exit",
"close_all_files": "Close All", "close_all_files": "Close All",
"save_all_files": "Save All", "save_all_files": "Save All",
"format": "Format", "format": "Format",
"goget": "go get",
"goinstall": "go install", "goinstall": "go install",
"build": "Build", "build": "Build",
"build_n_run": "Build&Run", "build_n_run": "Build&Run",
@ -37,9 +35,7 @@
"unread_notification": "Unread", "unread_notification": "Unread",
"notification_2": "Not found [gocode], thereby [Autocomplete] will not work", "notification_2": "Not found [gocode], thereby [Autocomplete] will not work",
"notification_3": "Not found [ide_stub], thereby [Jump to Decl], [Find Usages] will not work", "notification_3": "Not found [ide_stub], thereby [Jump to Decl], [Find Usages] will not work",
"notification_4": "Server Internal Error",
"goto_line": "Goto Line", "goto_line": "Goto Line",
"goto_file": "Goto File",
"go": "Go", "go": "Go",
"tip": "Tip", "tip": "Tip",
"confirm": "Confirm", "confirm": "Confirm",
@ -55,7 +51,7 @@
"find_previous": "Find Previous", "find_previous": "Find Previous",
"replace": "Replace", "replace": "Replace",
"replace_all": "Replace All", "replace_all": "Replace All",
"restore_bottom": "Restore Bottom Windows", "restore_bottom": "Bottom Windows Restore",
"file_format": "File Extension", "file_format": "File Extension",
"keyword": "Keyword", "keyword": "Keyword",
"user_guide": "User Guide", "user_guide": "User Guide",
@ -64,7 +60,7 @@
"ver": "Version", "ver": "Version",
"current_ver": "Current Version", "current_ver": "Current Version",
"dev_team": "Dev Team", "dev_team": "Dev Team",
"donate": "Donate", "donate_us": "Donate Us",
"confirm_save": "Confirm Save", "confirm_save": "Confirm Save",
"workspace": "Workspace", "workspace": "Workspace",
"project_address": "Project", "project_address": "Project",
@ -74,10 +70,6 @@
"show_expr_info": "Show Expression Info", "show_expr_info": "Show Expression Info",
"find_usages": "Find Usages", "find_usages": "Find Usages",
"delete_line": "Delete Line", "delete_line": "Delete Line",
"copy_lines_up": "Copy Lines Up",
"copy_lines_down": "Copy Lines Down",
"move_lines_up": "Move Lines Up",
"move_lines_down": "Move Lines Down",
"save_editor_file": "Save File", "save_editor_file": "Save File",
"save_all_editors_files": "Save All", "save_all_editors_files": "Save All",
"close_editor": "Close File", "close_editor": "Close File",
@ -94,13 +86,13 @@
"focus_notification": "Focus to Notification", "focus_notification": "Focus to Notification",
"start-build": "START [go build]", "start-build": "START [go build]",
"build-succ": "[go build] SUCCESS", "build-succ": "[go build] SUCCESS",
"build-error": "[go build] ERROR", "build-failed": "[go build] Failed",
"start-test": "START [go test]",
"test-succ": "[go test] SUCCESS",
"test-error": "[go test] ERROR",
"start-install": "START [go install]", "start-install": "START [go install]",
"install-succ": "[go install] SUCCESS", "install-succ": "[go install] SUCCESS",
"install-error": "[go install] ERROR", "install-failed": "[go install] Fialed",
"start-get": "START [go get]",
"get-succ": "[go get] SUCCESS",
"get-failed": "[go get] Failed",
"check_version": "Checking update", "check_version": "Checking update",
"new_version_available": "new version available", "new_version_available": "new version available",
"go_env": "Go", "go_env": "Go",
@ -109,63 +101,5 @@
"license": "License", "license": "License",
"credits": "Credits", "credits": "Credits",
"uptodate": "it is up to date", "uptodate": "it is up to date",
"test": "Test", "colon": ": "
"sign_up": "Sign Up",
"team": "Team",
"sing_up_error": "Sign Up Error",
"user_name_ruler": "Username only by az, AZ, 0-9, _ consisting of a length of 16",
"password_no_match": "Password doesn't match the confirmation",
"discard": "Discard",
"close": "Close",
"close_other": "Close Other",
"clear": "Clear",
"preference": "Preference",
"appearence": "Appearence",
"gotool": "Go Tool",
"user": "User",
"font": "Font",
"font_size": "Font Size",
"line_height": "Line Height",
"go_format": "Go Format",
"locale": "Locale",
"apply": "Apply",
"clearOutput": "Clear Output",
"export": "Export",
"refresh": "Refresh",
"theme": "Theme",
"tab_size": "Tab Size",
"copy_file_path": "Copy File Path",
"file_tree": "File Tree",
"select": "Select",
"expand": "Expand",
"collapse": "Collapse",
"edit": "Edit",
"undo": "Undo",
"redo": "Redo",
"cut": "Cut",
"copy": "Copy",
"paste": "Paste",
"select_all": "Select All",
"select_identifier": "Select Identifier",
"source": "Source",
"toggle_comment": "Toggle Comment",
"find_in_files": "Find in Files",
"no_empty": "Can not Empty!",
"open": "Open",
"search_no_match": "No matching files were found.",
"outline": "Outline",
"govet": "go vet",
"start-vet": "START [go vet]",
"vet-succ": "[go vet] SUCCESS",
"vet-error": "[go vet] ERROR",
"restore_outline": "Restore Outline",
"share": "Share",
"url": "URL",
"embeded": "Embeded",
"terms": "Terms",
"download": "Download",
"decompress": "Decompress",
"keymap": "Keymap",
"resize": "Resize",
"sponsor": "Sponsor"
} }

View File

@ -1,171 +0,0 @@
{
"colon": "",
"wide": "Wide",
"wide_title": "いつでも、どこでもゴランをプレイする",
"cancel": "取消",
"file": "ファイル",
"login": "ログイン",
"username": "ユーザ名",
"current_user": "現在のユーザ",
"current_session": "現在のセッション",
"password": "パスワード",
"login_error": "ログインエラー",
"run": "実行",
"debug": "デバッグ",
"help": "ヘルプ",
"check_update": "更新をチェック",
"issues": "問題",
"wide_doc": "Wide ドキュメント",
"about": "Wide について",
"start_page": "スタートページ",
"create_file": "新規ファイル",
"create": "作成",
"create_dir": "新規ディレクトリ",
"delete": "削除",
"rename": "名前の変更",
"save": "保存",
"exit": "終了",
"close_all_files": "全てのファイルを閉じる",
"save_all_files": "全てを保存",
"format": "フォーマット",
"goinstall": "go install",
"build": "ビルド",
"build_n_run": "ビルド実行",
"editor": "エディタ",
"max_editor": "最大化",
"restore_editor": "元のサイズに戻す",
"unread_notification": "未読の通知",
"notification_2": "[gocode] が見つかりません。[Autocomplete] は動作しません。",
"notification_3": "[ide_stub] が見つかりません。[Jump to Decl]、[Find Usages] は動作しません。",
"notification_4": "内部サーバーエラー",
"goto_line": "指定行にジャンプ",
"goto_file": "ファイルをオープンする",
"go": "Go",
"tip": "ヒント",
"confirm": "確認",
"stop": "停止",
"output": "出力",
"search": "検索",
"notification": "通知",
"min": "最小化",
"restore_side": "ファイルツリーを戻す",
"search_text": "テキストを検索",
"find": "検索",
"find_next": "次を検索",
"find_previous": "前を検索",
"replace": "置換",
"replace_all": "全て置換",
"restore_bottom": "ウィンドウを下に戻す",
"file_format": "ファイルの拡張子",
"keyword": "キーワード",
"user_guide": "ユーザガイド",
"dev_guide": "開発ガイド",
"keyboard_shortcuts": "キーボードショートカット",
"ver": "バージョン",
"current_ver": "現在のバージョン",
"dev_team": "開発チーム",
"donate": "寄付",
"confirm_save": "保存の確認",
"workspace": "ワークスペース",
"project_address": "プロジェクト",
"community": "コミュニティ",
"autocomplete": "自動補完",
"jump_to_decl": "定義へジャンプ",
"show_expr_info": "式の情報を表示",
"find_usages": "使用方法を検索する",
"delete_line": "行を削除",
"copy_lines_up": "フロントへのコピー",
"copy_lines_down": "一番下にコピー",
"move_lines_up": "前面に移動します",
"move_lines_down": "以下に移動",
"save_editor_file": "保存",
"save_all_editors_files": "全てを保存",
"close_editor": "エディタを閉じる",
"full_screen": "全画面",
"auto_indent": "自動インデント",
"indent": "インデントを増やす",
"unindent": "インデントを減らす",
"focus": "フォーカス",
"switch_tab": "タブを切り替える",
"focus_editor": "エディタにフォーカスを与える",
"focus_file_tree": "ファイルツリーにフォーカスを与える",
"focus_output": "出力にフォーカスを与える",
"focus_search": "検索にフォーカスを与える",
"focus_notification": "通知にフォーカスを与える",
"start-build": "[go build] 開始",
"build-succ": "[go build] 成功",
"build-error": "[go build] 失敗",
"start-test": "[go test] 開始",
"test-succ": "[go test] 成功",
"test-error": "[go test] 失敗",
"start-install": "[go install] 開始",
"install-succ": "[go install] 成功",
"install-error": "[go install] 失敗",
"check_version": "更新をチェック中",
"new_version_available": "新しいバージョンがあります",
"go_env": "Go",
"os": "OS",
"project": "プロジェクト",
"license": "ライセンス",
"credits": "クレジット",
"uptodate": "最新です",
"test": "テスト",
"sign_up": "登録",
"team": "チーム",
"sing_up_error": "登録に失敗しました",
"user_name_ruler": "16の長さからなる_ AZ、AZ、0-9、によってユーザ名のみ",
"password_no_match": "一貫性のないパスワード入力",
"discard": "あきらめる",
"close": "クローズ",
"close_other": "閉じるその他",
"clear": "空の",
"preference": "環境設定",
"appearence": "エクステリア",
"gotool": "Go ツール",
"user": "ユーザー",
"font": "フォント",
"font_size": "フォントサイズ",
"line_height": "行の高さ",
"go_format": "Go フォーマット",
"locale": "ロケール",
"apply": "適用する",
"clearOutput": "空の出力",
"export": "輸出",
"refresh": "リフレッシュ",
"theme": "テーマ",
"tab_size": "Tab サイズ",
"copy_file_path": "ファイルパスをコピー",
"file_tree": "ファイルツリー",
"select": "選択する",
"expand": "展開する",
"collapse": "シャットダウン",
"edit": "編集",
"undo": "元に戻す",
"redo": "やり直し",
"cut": "切り取り",
"copy": "コピー",
"paste": "貼り付け",
"select_all": "すべて選択",
"select_identifier": "選択識別子",
"source": "ソース",
"toggle_comment": "トグルコメント",
"find_in_files": "ファイルから検索",
"no_empty": "空ではありません",
"open": "オープン",
"search_no_match": "一致するファイルが見つかりませんでした。",
"outline": "アウトライン",
"govet": "go vet",
"start-vet": "[go vet] 開始",
"vet-succ": "[go vet] 成功",
"vet-error": "[go vet] 失敗",
"restore_outline": "アウトラインを復元",
"share": "シェア",
"url": "リンク",
"embeded": "埋め込む",
"terms": "利用規約",
"download": "ダウンロード",
"decompress": "解凍する",
"keymap": "キーマップ",
"resize": "サイズ変更",
"sponsor": "スポンサー"
}

View File

@ -1,171 +0,0 @@
{
"colon": "",
"wide": "Wide",
"wide_title": "언제 어디서나 골란 놀기",
"cancel": "취소",
"file": "문서",
"login": "로그인",
"username": "아이디",
"current_user": "현제 접속자 ",
"current_session": "현제 세션",
"password": "비밀번호",
"login_error": "로그인 실패",
"run": "실행",
"debug": "디버깅",
"help": "도움말",
"check_update": "업데이트 확인",
"issues": "문제",
"wide_doc": "Wide 문서",
"about": "about",
"start_page": "시작 페이지",
"create_file": "새문서",
"create": "새로만들기",
"create_dir": "새폴더",
"delete": "삭제",
"rename": "이름변경",
"save": "저장",
"exit": "끝내기",
"close_all_files": "모두 닫기",
"save_all_files": "일괄저장",
"format": "포멧",
"goinstall": "go install",
"build": "빌드",
"build_n_run": "빌드후실행",
"editor": "편집기",
"max_editor": "최대화",
"restore_editor": "복구",
"unread_notification": "읽지않은 공지",
"notification_2": "[gocode] 를 찾지 못하였습니다. 자동완성기능이 동작하지 않습니다. ",
"notification_3": "[ide_stub] 를 찾지 못하였습니다. 찾기 기능이 동작하지 않습니다. ",
"notification_4": "서버 오류",
"goto_line": "라인이동",
"goto_file": "문서오픈",
"go": "이동",
"tip": "팁",
"confirm": "확인",
"stop": "정지",
"output": "출력",
"search": "검색",
"notification": "알림",
"min": "최소화",
"restore_side": "좌측창 복구",
"search_text": "문서검색",
"find": "검색",
"find_next": "다음검색",
"find_previous": "이전검색",
"replace": "바꾸기",
"replace_all": "전부바꾸기",
"restore_bottom": "아래창 복구",
"file_format": "확장자",
"keyword": "키워드",
"user_guide": "이용가이드",
"dev_guide": "개발 가이드",
"keyboard_shortcuts": "단축키",
"ver": "버전",
"current_ver": "현재버전",
"dev_team": "개발단체",
"donate": "기부",
"confirm_save": "정장했는지 확인해 주세요. ",
"workspace": "작업창",
"project_address": "항목주소",
"community": "커뮤니티",
"autocomplete": "자동완성",
"jump_to_decl": "알림으로 이동",
"show_expr_info": "Expression 보기",
"find_usages": "사용법찾기",
"delete_line": "현재 줄 삭제",
"copy_lines_up": "위로 복사",
"copy_lines_down": "아래로 복사",
"move_lines_up": "위로이동",
"move_lines_down": "아래로 이동",
"save_editor_file": "현재 편집하고있던 문서 저장",
"save_all_editors_files": "일괄저장",
"close_editor": "편집창 닫기",
"full_screen": "편집기 전체화면",
"auto_indent": "자동 정렬",
"indent": "안쪽이동",
"unindent": "밖으로 이동",
"focus": "포커스",
"switch_tab": "편집기편집/창편집 tab",
"focus_editor": "편집창으로 포커스",
"focus_file_tree": "프로젝트트리로 포커스이동",
"focus_output": "output 으로 포커스 이동",
"focus_search": "검색창으로 포커스 이동",
"focus_notification": "알림창으로 포커스 이동",
"start-build": "시작 [go build]",
"build-succ": "[go build] 성공",
"build-error": "[go build] 실패",
"start-test": "시작 [go test]",
"test-succ": "[go test] 성공",
"test-error": "[go test] 실패",
"start-install": "시작 [go install]",
"install-succ": "[go install] 성공",
"install-error": "[go install] 실패",
"check_version": "최신버전검색중",
"new_version_available": "최신업데이트 사용 가능",
"go_env": "Go 환경",
"os": "운영체제",
"project": "항목",
"license": "라이센스",
"credits": "감사합니다.",
"uptodate": "최신버전입니다.",
"test": "테스트",
"sign_up": "회원가입",
"team": "단체",
"sing_up_error": "가입실패",
"user_name_ruler": "아이디는 16글자 이하이며 a-z, A-Z, 0-9, _ 만 가능합니다,",
"password_no_match": "비밀번호 오류",
"discard": "취소",
"close": "닫기",
"close_other": "현재창 남기고 닫기",
"clear": "청소",
"preference": "설정",
"appearence": "외관",
"gotool": "Go 도구",
"user": "유저",
"font": "폰트",
"font_size": "폰트크기",
"line_height": "줄간격",
"go_format": "Go 포멧",
"locale": "언어설정",
"apply": "적용",
"clearOutput": "ouput 클리어",
"export": "내보내기",
"refresh": "새로고침",
"theme": "주제",
"tab_size": "Tab 크기",
"copy_file_path": "경로복사",
"file_tree": "트리",
"select": "선택",
"expand": "확장",
"collapse": "축소",
"edit": "편집",
"undo": "취소",
"redo": "복원",
"cut": "잘라내기",
"copy": "복사",
"paste": "붙여넣기",
"select_all": "전체선택",
"select_identifier": "표식선택",
"source": "소스",
"toggle_comment": "주석",
"find_in_files": "문서에서 찾기",
"no_empty": "값을 입력해 주세요.",
"open": "열기",
"search_no_match": "해당 문서를 찾지 못하였습니다.",
"outline": "주제",
"govet": "go vet",
"start-vet": "시작 [go vet]",
"vet-succ": "[go vet] 성공",
"vet-error": "[go vet] 실패",
"restore_outline": "주제복구",
"share": "공유",
"url": "하이퍼링크",
"embeded": "삽입",
"terms": "사용계약",
"download": "다운로드",
"decompress": "압축풀기",
"keymap": "단축키",
"resize": "크기조절",
"sponsor": "후원사"
}

View File

@ -1,66 +1,35 @@
// Copyright (c) 2014-present, b3log.org // 国际化操作.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package i18n includes internationalization related manipulations.
package i18n package i18n
import ( import (
"encoding/json" "encoding/json"
"io/ioutil"
"os" "os"
"sort"
"strings"
"github.com/88250/gulu" "github.com/golang/glog"
) )
// Logger.
var logger = gulu.Log.NewLogger(os.Stdout)
// Locale.
type locale struct { type locale struct {
Name string Name string
Langs map[string]interface{} Langs map[string]interface{}
TimeZone string TimeZone string
} }
// All locales. // 所有的 locales.
var Locales = map[string]locale{} var Locales = map[string]locale{}
// Load loads i18n message configurations. // 加载国际化配置.
func Load() { func Load() {
f, _ := os.Open("i18n") // TODO: 自动加载所有语言配置
names, _ := f.Readdirnames(-1)
f.Close()
if len(Locales) == len(names)-1 { load("zh_CN")
return load("en_US")
}
for _, name := range names {
if !strings.HasSuffix(name, ".json") {
continue
}
loc := name[:strings.LastIndex(name, ".")]
load(loc)
}
} }
func load(localeStr string) { func load(localeStr string) {
bytes, err := os.ReadFile("i18n/" + localeStr + ".json") bytes, err := ioutil.ReadFile("i18n/" + localeStr + ".json")
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
os.Exit(-1) os.Exit(-1)
} }
@ -69,33 +38,22 @@ func load(localeStr string) {
err = json.Unmarshal(bytes, &l.Langs) err = json.Unmarshal(bytes, &l.Langs)
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
os.Exit(-1) os.Exit(-1)
} }
Locales[localeStr] = l Locales[localeStr] = l
glog.V(5).Infof("Loaded [%s] locale configuration", localeStr)
} }
// Get gets a message with the specified locale and key. // 获取语言配置项.
func Get(locale, key string) interface{} { func Get(locale, key string) interface{} {
return Locales[locale].Langs[key] return Locales[locale].Langs[key]
} }
// GetAll gets all messages with the specified locale. // 获取语言配置.
func GetAll(locale string) map[string]interface{} { func GetAll(locale string) map[string]interface{} {
return Locales[locale].Langs return Locales[locale].Langs
} }
// GetLocalesNames gets names of all locales. Returns ["zh_CN", "en_US"] for example.
func GetLocalesNames() []string {
ret := []string{}
for name := range Locales {
ret = append(ret, name)
}
sort.Strings(ret)
return ret
}

View File

@ -1,20 +1,18 @@
{ {
"colon": "",
"wide": "Wide", "wide": "Wide",
"wide_title": "随时随地玩 golang", "isDelete": "是否删除",
"cancel": "取消", "cancel": "取消",
"file": "文件", "file": "文件",
"login": "登录", "login": "登录",
"username": "用户名", "username": "用户名",
"current_user": "当前用户", "current_user": "当前用户",
"current_session": "当前会话",
"password": "密码", "password": "密码",
"login_error": "登录失败", "login_failed": "登录失败",
"run": "运行", "run": "运行",
"debug": "调试", "debug": "调试",
"help": "帮助", "help": "帮助",
"check_update": "检查更新", "check_update": "检查更新",
"issues": "问题", "report_issues": "建议",
"wide_doc": "Wide 文档", "wide_doc": "Wide 文档",
"about": "关于", "about": "关于",
"start_page": "起始页", "start_page": "起始页",
@ -22,12 +20,12 @@
"create": "创建", "create": "创建",
"create_dir": "创建目录", "create_dir": "创建目录",
"delete": "删除", "delete": "删除",
"rename": "重命名",
"save": "保存", "save": "保存",
"exit": "退出", "exit": "退出",
"close_all_files": "关闭所有文件", "close_all_files": "关闭所有文件",
"save_all_files": "保存所有文件", "save_all_files": "保存所有文件",
"format": "格式化", "format": "格式化",
"goget": "go get",
"goinstall": "go install", "goinstall": "go install",
"build": "构建", "build": "构建",
"build_n_run": "构建并运行", "build_n_run": "构建并运行",
@ -37,9 +35,7 @@
"unread_notification": "未读通知", "unread_notification": "未读通知",
"notification_2": "没有检查到 gocode这将会导致 [自动完成] 失效", "notification_2": "没有检查到 gocode这将会导致 [自动完成] 失效",
"notification_3": "没有检查到 ide_stub这将会导致 [跳转到声明]、[查找使用] 失效", "notification_3": "没有检查到 ide_stub这将会导致 [跳转到声明]、[查找使用] 失效",
"notification_4": "服务器内部错误",
"goto_line": "跳转到行", "goto_line": "跳转到行",
"goto_file": "打开文件",
"go": "跳转", "go": "跳转",
"tip": "提示", "tip": "提示",
"confirm": "确定", "confirm": "确定",
@ -64,7 +60,7 @@
"ver": "版本", "ver": "版本",
"current_ver": "当前版本", "current_ver": "当前版本",
"dev_team": "开发团队", "dev_team": "开发团队",
"donate": "捐赠我们", "donate_us": "捐赠我们",
"confirm_save": "请确认所有文件已保存", "confirm_save": "请确认所有文件已保存",
"workspace": "工作空间", "workspace": "工作空间",
"project_address": "项目地址", "project_address": "项目地址",
@ -74,10 +70,6 @@
"show_expr_info": "查看表达式信息", "show_expr_info": "查看表达式信息",
"find_usages": "查找使用", "find_usages": "查找使用",
"delete_line": "删除当前行", "delete_line": "删除当前行",
"copy_lines_up": "复制到上方",
"copy_lines_down": "复制到下方",
"move_lines_up": "移动到上方",
"move_lines_down": "移动到下方",
"save_editor_file": "保存当前编辑器文件", "save_editor_file": "保存当前编辑器文件",
"save_all_editors_files": "保存所有编辑器文件", "save_all_editors_files": "保存所有编辑器文件",
"close_editor": "关闭当前编辑器", "close_editor": "关闭当前编辑器",
@ -94,13 +86,13 @@
"focus_notification": "焦点切换到通知窗口", "focus_notification": "焦点切换到通知窗口",
"start-build": "开始 [go build]", "start-build": "开始 [go build]",
"build-succ": "[go build] 成功", "build-succ": "[go build] 成功",
"build-error": "[go build] 失败", "build-failed": "[go build] 失败",
"start-test": "开始 [go test]",
"test-succ": "[go test] 成功",
"test-error": "[go test] 失败",
"start-install": "开始 [go install]", "start-install": "开始 [go install]",
"install-succ": "[go install] 成功", "install-succ": "[go install] 成功",
"install-error": "[go install] 失败", "install-failed": "[go install] 失败",
"start-get": "开始 [go get]",
"get-succ": "[go get] 成功",
"get-failed": "[go get] 失败",
"check_version": "正在检查更新", "check_version": "正在检查更新",
"new_version_available": "新版本可用", "new_version_available": "新版本可用",
"go_env": "Go 环境", "go_env": "Go 环境",
@ -109,63 +101,5 @@
"license": "许可协议", "license": "许可协议",
"credits": "致谢", "credits": "致谢",
"uptodate": "已是最新版本", "uptodate": "已是最新版本",
"test": "测试", "colon": ""
"sign_up": "注册",
"team": "团队",
"sing_up_error": "注册失败",
"user_name_ruler": "用户名只能由 a-z, A-Z, 0-9, _ 组成长度为16",
"password_no_match": "密码输入不一致",
"discard": "放弃",
"close": "关闭",
"close_other": "关闭其它",
"clear": "清空",
"preference": "偏好设定",
"appearence": "外观",
"gotool": "Go 工具",
"user": "用户",
"font": "字体",
"font_size": "字体大小",
"line_height": "行高",
"go_format": "Go 格式化",
"locale": "语言环境",
"apply": "应用",
"clearOutput": "清空输出",
"export": "导出",
"refresh": "刷新",
"theme": "主题",
"tab_size": "Tab 大小",
"copy_file_path": "复制文件路径",
"file_tree": "文件树",
"select": "选择",
"expand": "展开",
"collapse": "收起",
"edit": "编辑",
"undo": "撤销",
"redo": "重做",
"cut": "剪切",
"copy": "复制",
"paste": "粘贴",
"select_all": "全选",
"select_identifier": "选择标识符",
"source": "源码",
"toggle_comment": "注释",
"find_in_files": "在文件中查找",
"no_empty": "不能为空",
"open": "打开",
"search_no_match": "没有发现匹配的文件。",
"outline": "大纲",
"govet": "go vet",
"start-vet": "开始 [go vet]",
"vet-succ": "[go vet] 成功",
"vet-error": "[go vet] 失败",
"restore_outline": "恢复大纲",
"share": "分享",
"url": "链接",
"embeded": "嵌入",
"terms": "使用条款",
"download": "下载",
"decompress": "解压缩",
"keymap": "快捷键",
"resize": "调整大小",
"sponsor": "赞助"
} }

View File

@ -1,171 +0,0 @@
{
"colon": "",
"wide": "Wide",
"wide_title": "隨時隨地玩 golang",
"cancel": "取消",
"file": "檔案",
"login": "登入",
"username": "使用者",
"current_user": "當前使用者",
"current_session": "當前會話",
"password": "密碼",
"login_error": "登入失敗",
"run": "執行",
"debug": "Debug",
"help": "說明書",
"check_update": "檢查更新?",
"issues": "問題",
"wide_doc": "Wide 說明",
"about": "關於",
"start_page": "開始頁面",
"create_file": "開新檔案",
"create": "新建",
"create_dir": "新增資料夾",
"delete": "删除",
"rename": "重新命名",
"save": "儲存",
"exit": "離開",
"close_all_files": "關閉所有檔案",
"save_all_files": "儲存所有檔案",
"format": "格式化",
"goinstall": "go install",
"build": "編譯",
"build_n_run": "編譯並執行",
"editor": "編輯器",
"max_editor": "編輯器最大化",
"restore_editor": "編輯器還原",
"unread_notification": "未讀通知",
"notification_2": "没有檢查到 gocode這將會導致「自動完成」失效",
"notification_3": "没有檢查到 ide_stub這將會導致「跳轉到聲明」、「查找使用」失效",
"notification_4": "伺服器內部錯誤",
"goto_line": "跳轉到行",
"goto_file": "開啟舊檔",
"go": "跳到",
"tip": "提示",
"confirm": "確定",
"stop": "停止",
"output": "輸出",
"search": "搜尋",
"notification": "通知",
"min": "縮到最小",
"restore_side": "左側視窗還原",
"search_text": "尋找",
"find": "尋找",
"find_next": "尋找下一個",
"find_previous": "尋找上一個",
"replace": "取代",
"replace_all": "取代全部",
"restore_bottom": "底部視窗還原",
"file_format": "文件格式",
"keyword": "關鍵字",
"user_guide": "使用者說明文件",
"dev_guide": "開發說明文件",
"keyboard_shortcuts": "鍵盤快捷鍵",
"ver": "版本",
"current_ver": "當前版本",
"dev_team": "開發團隊",
"donate_us": "愛心捐贈",
"confirm_save": "請確認所有檔案都已儲存",
"workspace": "工作空間",
"project_address": "項目地址",
"community": "社區",
"autocomplete": "自動完成",
"jump_to_decl": "跳轉到聲明",
"show_expr_info": "查看表達式信息",
"find_usages": "尋找使用",
"delete_line": "删除當前行",
"copy_lines_up": "複製到上一行",
"copy_lines_down": "複製到下一行",
"move_lines_up": "移動到上一行",
"move_lines_down": "移動到下一行",
"save_editor_file": "儲存當前編輯檔案",
"save_all_editors_files": "儲存所有檔案",
"close_editor": "關閉當前編輯器",
"full_screen": "全螢幕",
"auto_indent": "自動縮進",
"indent": "縮進",
"unindent": "縮進還原",
"focus": "焦點",
"switch_tab": "切換編輯器/視窗组 tab",
"focus_editor": "切換至編輯器",
"focus_file_tree": "切換至檔案樹",
"focus_output": "切換至输出視窗",
"focus_search": "切換至搜索視窗",
"focus_notification": "切換至通知視窗",
"start-build": "開始 [go build]",
"build-succ": "[go build] 成功",
"build-error": "[go build] 失敗",
"start-test": "開始 [go test]",
"test-succ": "[go test] 成功",
"test-error": "[go test] 失敗",
"start-install": "開始 [go install]",
"install-succ": "[go install] 成功",
"install-error": "[go install] 失敗",
"check_version": "正在檢查更新",
"new_version_available": "可用新版本",
"go_env": "Go 環境",
"os": "操作系统",
"project": "項目",
"license": "許可協議",
"credits": "致謝",
"uptodate": "已是最新版本",
"test": "測試",
"sign_up": "註冊",
"team": "團隊",
"sing_up_error": "註冊失敗",
"user_name_ruler": "帳號只能由 az, AZ, 0-9, _ 組成長度為16",
"password_no_match": "密碼輸入不一致",
"discard": "捨棄",
"close": "關閉",
"close_other": "關閉其它",
"clear": "清空",
"preference": "偏好設定",
"appearence": "外觀",
"gotool": "Go 工具",
"user": "使用者",
"font": "字體",
"font_size": "字體大小",
"line_height": "行高",
"go_format": "Go 格式化",
"locale": "語言環境",
"apply": "應用",
"clearOutput": "清空輸出",
"export": "導出",
"refresh": "刷新",
"theme": "主題",
"tab_size": "Tab 大小",
"copy_file_path": "複製檔案位置",
"file_tree": "文件樹",
"select": "選擇",
"expand": "展開",
"collapse": "收起",
"edit": "編輯",
"undo": "復原",
"redo": "回復",
"cut": "剪下",
"copy": "複製",
"paste": "天上",
"select_all": "全選",
"select_identifier": "選擇標識符",
"source": "原始碼",
"toggle_comment": "註解",
"find_in_files": "在文件中尋找",
"no_empty": "不能為空",
"open": "開啟",
"search_no_match": "沒有發現匹配的文件。",
"outline": "大綱",
"govet": "go vet",
"start-vet": "開始 [go vet]",
"vet-succ": "[go vet] 成功",
"vet-error": "[go vet] 失敗",
"restore_outline": "恢復大綱",
"share": "分享",
"url": "連結",
"embeded": "嵌入",
"terms": "使用條款",
"download": "下載",
"decompress": "解壓縮",
"keymap": "快速鍵",
"resize": "調整大小",
"sponsor": "贊助"
}

549
main.go
View File

@ -1,216 +1,163 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main package main
import ( import (
"compress/gzip" "encoding/json"
"flag" "flag"
"html/template" "html/template"
"io" "math/rand"
"mime"
"net/http" "net/http"
_ "net/http/pprof"
"os"
"os/signal"
"runtime" "runtime"
"strings" "strconv"
"syscall"
"time" "time"
"github.com/88250/gulu" "github.com/b3log/wide/conf"
"github.com/88250/wide/conf" "github.com/b3log/wide/editor"
"github.com/88250/wide/editor" "github.com/b3log/wide/event"
"github.com/88250/wide/event" "github.com/b3log/wide/file"
"github.com/88250/wide/file" "github.com/b3log/wide/i18n"
"github.com/88250/wide/i18n" "github.com/b3log/wide/notification"
"github.com/88250/wide/notification" "github.com/b3log/wide/output"
"github.com/88250/wide/output" "github.com/b3log/wide/session"
"github.com/88250/wide/playground" "github.com/b3log/wide/shell"
"github.com/88250/wide/session" "github.com/b3log/wide/util"
"github.com/golang/glog"
) )
// Logger const (
var logger *gulu.Logger Ver = "1.0.0" // 当前 Wide 版本
)
// The only one init function in Wide. // Wide 中唯一一个 init 函数.
func init() { func init() {
confPath := flag.String("conf", "conf/wide.json", "path of wide.json") // TODO: 默认启动参数
confData := flag.String("data", "", "path of data dir") flag.Set("logtostderr", "true")
confServer := flag.String("server", "", "this will overwrite Wide.Server if specified") flag.Set("v", "3")
confLogLevel := flag.String("log_level", "", "this will overwrite Wide.LogLevel if specified")
confReadOnly := flag.String("readonly", "", "this will overrite Wide.ReadOnly if specified")
confSiteStatCode := flag.String("site_stat_code", "", "this will overrite Wide.SiteStatCode if specified")
flag.Parse() flag.Parse()
gulu.Log.SetLevel("warn") // 加载事件处理
logger = gulu.Log.NewLogger(os.Stdout)
//wd := gulu.OS.Pwd()
//if strings.HasPrefix(wd, os.TempDir()) {
// logger.Error("Don't run Wide in OS' temp directory or with `go run`")
//
// os.Exit(-1)
//}
i18n.Load()
event.Load() event.Load()
conf.Load(*confPath, *confData, *confServer, *confLogLevel, *confReadOnly, template.HTML(*confSiteStatCode))
// 加载配置
conf.Load()
// 定时检查运行环境
conf.FixedTimeCheckEnv() conf.FixedTimeCheckEnv()
session.FixedTimeSave()
// 定时保存配置
conf.FixedTimeSave()
// 定时检查无效会话
session.FixedTimeRelease() session.FixedTimeRelease()
session.FixedTimeReport()
logger.Debug("host [" + runtime.Version() + ", " + runtime.GOOS + "_" + runtime.GOARCH + "]")
} }
// Main. // 登录.
func main() { func loginHandler(w http.ResponseWriter, r *http.Request) {
initMime() i18n.Load()
handleSignal()
// IDE if "GET" == r.Method {
http.HandleFunc("/", handlerGzWrapper(indexHandler)) // 展示登录页面
http.HandleFunc("/start", handlerWrapper(startHandler))
http.HandleFunc("/about", handlerWrapper(aboutHandler))
http.HandleFunc("/keyboard_shortcuts", handlerWrapper(keyboardShortcutsHandler))
// static resources model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(conf.Wide.Locale),
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) "locale": conf.Wide.Locale, "ver": Ver}
http.Handle("/static/users/", http.StripPrefix("/static/", http.FileServer(http.Dir(conf.Wide.Data+"/static"))))
serveSingle("/favicon.ico", "./static/images/favicon.png")
// oauth t, err := template.ParseFiles("views/login.html")
http.HandleFunc("/login/redirect", session.LoginRedirectHandler)
http.HandleFunc("/login/callback", session.LoginCallbackHandler)
// session if nil != err {
http.HandleFunc("/session/ws", handlerWrapper(session.WSHandler)) glog.Error(err)
http.HandleFunc("/session/save", handlerWrapper(session.SaveContentHandler)) http.Error(w, err.Error(), 500)
// run return
http.HandleFunc("/build", handlerWrapper(output.BuildHandler)) }
http.HandleFunc("/run", handlerWrapper(output.RunHandler))
http.HandleFunc("/stop", handlerWrapper(output.StopHandler))
http.HandleFunc("/go/test", handlerWrapper(output.GoTestHandler))
http.HandleFunc("/go/vet", handlerWrapper(output.GoVetHandler))
http.HandleFunc("/go/install", handlerWrapper(output.GoInstallHandler))
http.HandleFunc("/output/ws", handlerWrapper(output.WSHandler))
// cross-compilation t.Execute(w, model)
http.HandleFunc("/cross", handlerWrapper(output.CrossCompilationHandler))
// file tree return
http.HandleFunc("/files", handlerWrapper(file.GetFilesHandler))
http.HandleFunc("/file/refresh", handlerWrapper(file.RefreshDirectoryHandler))
http.HandleFunc("/file", handlerWrapper(file.GetFileHandler))
http.HandleFunc("/file/save", handlerWrapper(file.SaveFileHandler))
http.HandleFunc("/file/new", handlerWrapper(file.NewFileHandler))
http.HandleFunc("/file/remove", handlerWrapper(file.RemoveFileHandler))
http.HandleFunc("/file/rename", handlerWrapper(file.RenameFileHandler))
http.HandleFunc("/file/search/text", handlerWrapper(file.SearchTextHandler))
http.HandleFunc("/file/find/name", handlerWrapper(file.FindHandler))
// outline
http.HandleFunc("/outline", handlerWrapper(file.GetOutlineHandler))
// file export
http.HandleFunc("/file/zip/new", handlerWrapper(file.CreateZipHandler))
http.HandleFunc("/file/zip", handlerWrapper(file.GetZipHandler))
// editor
http.HandleFunc("/go/fmt", handlerWrapper(editor.GoFmtHandler))
http.HandleFunc("/autocomplete", handlerWrapper(editor.AutocompleteHandler))
http.HandleFunc("/exprinfo", handlerWrapper(editor.GetExprInfoHandler))
http.HandleFunc("/find/decl", handlerWrapper(editor.FindDeclarationHandler))
http.HandleFunc("/find/usages", handlerWrapper(editor.FindUsagesHandler))
// notification
http.HandleFunc("/notification/ws", handlerWrapper(notification.WSHandler))
// user
http.HandleFunc("/login", handlerWrapper(session.LoginHandler))
http.HandleFunc("/logout", handlerWrapper(session.LogoutHandler))
http.HandleFunc("/preference", handlerWrapper(session.PreferenceHandler))
// playground
http.HandleFunc("/playground", handlerWrapper(playground.IndexHandler))
http.HandleFunc("/playground/", handlerWrapper(playground.IndexHandler))
http.HandleFunc("/playground/ws", handlerWrapper(playground.WSHandler))
http.HandleFunc("/playground/save", handlerWrapper(playground.SaveHandler))
http.HandleFunc("/playground/build", handlerWrapper(playground.BuildHandler))
http.HandleFunc("/playground/run", handlerWrapper(playground.RunHandler))
http.HandleFunc("/playground/stop", handlerWrapper(playground.StopHandler))
http.HandleFunc("/playground/autocomplete", handlerWrapper(playground.AutocompleteHandler))
logger.Infof("Wide is running [%s]", conf.Wide.Server)
err := http.ListenAndServe("0.0.0.0:7070", nil)
if err != nil {
logger.Error(err)
} }
// 非 GET 请求当作是登录请求
succ := false
data := map[string]interface{}{"succ": &succ}
defer util.RetJSON(w, r, data)
args := struct {
Username string
Password string
}{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
glog.Error(err)
succ = true
return
}
for _, user := range conf.Wide.Users {
if user.Name == args.Username && user.Password == args.Password {
succ = true
}
}
if !succ {
return
}
// 创建 HTTP 会话
httpSession, _ := session.HTTPSession.Get(r, "wide-session")
httpSession.Values["username"] = args.Username
httpSession.Values["id"] = strconv.Itoa(rand.Int())
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
httpSession.Save(r, w)
glog.Infof("Created a HTTP session [%s] for user [%s]", httpSession.Values["id"].(string), args.Username)
} }
// indexHandler handles request of Wide index. // 退出(登出).
func logoutHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{"succ": true}
defer util.RetJSON(w, r, data)
httpSession, _ := session.HTTPSession.Get(r, "wide-session")
httpSession.Options.MaxAge = -1
httpSession.Save(r, w)
}
// Wide 首页.
func indexHandler(w http.ResponseWriter, r *http.Request) { func indexHandler(w http.ResponseWriter, r *http.Request) {
if "/" != r.RequestURI { i18n.Load()
http.Redirect(w, r, "/", http.StatusFound)
return httpSession, _ := session.HTTPSession.Get(r, "wide-session")
}
httpSession, _ := session.HTTPSession.Get(r, session.CookieName)
if httpSession.IsNew { if httpSession.IsNew {
http.Redirect(w, r, "/login", http.StatusFound) http.Redirect(w, r, "/login", http.StatusFound)
return return
} }
uid := httpSession.Values["uid"].(string)
if "playground" == uid { // reserved user for Playground
http.Redirect(w, r, "/login", http.StatusFound)
return
}
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
httpSession.Save(r, w) httpSession.Save(r, w)
user := conf.GetUser(uid) // 创建一个 Wide 会话
if nil == user { wideSession := session.WideSessions.New(httpSession)
http.Redirect(w, r, "/login", http.StatusFound)
return username := httpSession.Values["username"].(string)
} locale := conf.Wide.GetUser(username).Locale
locale := user.Locale wideSessions := session.WideSessions.GetByUsername(username)
userConf := conf.Wide.GetUser(username)
wideSessions := session.WideSessions.GetByUserId(uid)
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale, model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale,
"uid": uid, "sid": session.WideSessions.GenId(), "latestSessionContent": user.LatestSessionContent, "session": wideSession, "latestSessionContent": userConf.LatestSessionContent,
"pathSeparator": conf.PathSeparator, "codeMirrorVer": conf.CodeMirrorVer, "pathSeparator": conf.PathSeparator}
"user": user, "editorThemes": conf.GetEditorThemes(), "crossPlatforms": []string{"darwin_amd64", "linux_amd64", "windows_amd64"}}
logger.Debugf("User [%s] has [%d] sessions", uid, len(wideSessions)) glog.V(3).Infof("User [%s] has [%d] sessions", username, len(wideSessions))
t, err := template.ParseFiles("views/index.html") t, err := template.ParseFiles("views/index.html")
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), 500)
return return
} }
@ -218,70 +165,19 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
t.Execute(w, model) t.Execute(w, model)
} }
// handleSignal handles system signal for graceful shutdown. // 单个文件资源请求处理.
func handleSignal() {
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
s := <-c
logger.Tracef("Got signal [%s]", s)
session.SaveOnlineUsers()
logger.Tracef("Saved all online user, exit")
os.Exit(0)
}()
}
// serveSingle registers the handler function for the given pattern and filename.
func serveSingle(pattern string, filename string) { func serveSingle(pattern string, filename string) {
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, filename) http.ServeFile(w, r, filename)
}) })
} }
// startHandler handles request of start page. // 起始页请求处理.
func startHandler(w http.ResponseWriter, r *http.Request) { func startHandler(w http.ResponseWriter, r *http.Request) {
httpSession, _ := session.HTTPSession.Get(r, session.CookieName) i18n.Load()
if httpSession.IsNew {
http.Redirect(w, r, "/s", http.StatusFound)
return httpSession, _ := session.HTTPSession.Get(r, "wide-session")
}
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
httpSession.Save(r, w)
uid := httpSession.Values["uid"].(string)
user := conf.GetUser(uid)
locale := user.Locale
userWorkspace := conf.GetUserWorkspace(uid)
sid := r.URL.Query()["sid"][0]
wSession := session.WideSessions.Get(sid)
if nil == wSession {
logger.Errorf("Session [%s] not found", sid)
}
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale,
"uid": uid, "workspace": userWorkspace, "ver": conf.WideVersion, "sid": sid, "username": user.Name}
t, err := template.ParseFiles("views/start.html")
if nil != err {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
t.Execute(w, model)
}
// keyboardShortcutsHandler handles request of keyboard shortcuts page.
func keyboardShortcutsHandler(w http.ResponseWriter, r *http.Request) {
httpSession, _ := session.HTTPSession.Get(r, session.CookieName)
if httpSession.IsNew { if httpSession.IsNew {
http.Redirect(w, r, "/login", http.StatusFound) http.Redirect(w, r, "/login", http.StatusFound)
@ -291,16 +187,50 @@ func keyboardShortcutsHandler(w http.ResponseWriter, r *http.Request) {
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
httpSession.Save(r, w) httpSession.Save(r, w)
uid := httpSession.Values["uid"].(string) username := httpSession.Values["username"].(string)
locale := conf.GetUser(uid).Locale locale := conf.Wide.GetUser(username).Locale
userWorkspace := conf.Wide.GetUserWorkspace(username)
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale,
"username": username, "workspace": userWorkspace, "ver": Ver}
t, err := template.ParseFiles("views/start.html")
if nil != err {
glog.Error(err)
http.Error(w, err.Error(), 500)
return
}
t.Execute(w, model)
}
// 键盘快捷键页请求处理.
func keyboardShortcutsHandler(w http.ResponseWriter, r *http.Request) {
i18n.Load()
httpSession, _ := session.HTTPSession.Get(r, "wide-session")
if httpSession.IsNew {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
httpSession.Save(r, w)
username := httpSession.Values["username"].(string)
locale := conf.Wide.GetUser(username).Locale
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale} model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale}
t, err := template.ParseFiles("views/keyboard_shortcuts.html") t, err := template.ParseFiles("views/keyboard_shortcuts.html")
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), 500)
return return
} }
@ -308,9 +238,12 @@ func keyboardShortcutsHandler(w http.ResponseWriter, r *http.Request) {
t.Execute(w, model) t.Execute(w, model)
} }
// aboutHandle handles request of about page. // 关于页请求处理.
func aboutHandler(w http.ResponseWriter, r *http.Request) { func aboutHandler(w http.ResponseWriter, r *http.Request) {
httpSession, _ := session.HTTPSession.Get(r, session.CookieName) i18n.Load()
httpSession, _ := session.HTTPSession.Get(r, "wide-session")
if httpSession.IsNew { if httpSession.IsNew {
http.Redirect(w, r, "/login", http.StatusFound) http.Redirect(w, r, "/login", http.StatusFound)
@ -320,17 +253,17 @@ func aboutHandler(w http.ResponseWriter, r *http.Request) {
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
httpSession.Save(r, w) httpSession.Save(r, w)
uid := httpSession.Values["uid"].(string) username := httpSession.Values["username"].(string)
locale := conf.GetUser(uid).Locale locale := conf.Wide.GetUser(username).Locale
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale, model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale, "ver": Ver,
"ver": conf.WideVersion, "goos": runtime.GOOS, "goarch": runtime.GOARCH, "gover": runtime.Version()} "goos": runtime.GOOS, "goarch": runtime.GOARCH, "gover": runtime.Version()}
t, err := template.ParseFiles("views/about.html") t, err := template.ParseFiles("views/about.html")
if nil != err { if nil != err {
logger.Error(err) glog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), 500)
return return
} }
@ -338,104 +271,112 @@ func aboutHandler(w http.ResponseWriter, r *http.Request) {
t.Execute(w, model) t.Execute(w, model)
} }
// handlerWrapper wraps the HTTP Handler for some common processes. // 主程序入口.
func main() {
runtime.GOMAXPROCS(conf.Wide.MaxProcs)
defer glog.Flush()
// IDE
http.HandleFunc("/login", handlerWrapper(loginHandler))
http.HandleFunc("/logout", handlerWrapper(logoutHandler))
http.HandleFunc("/", handlerWrapper(indexHandler))
http.HandleFunc("/start", handlerWrapper(startHandler))
http.HandleFunc("/about", handlerWrapper(aboutHandler))
http.HandleFunc("/keyboard_shortcuts", handlerWrapper(keyboardShortcutsHandler))
// 静态资源
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
serveSingle("/favicon.ico", "./static/favicon.ico")
// 库资源
http.Handle("/data/", http.StripPrefix("/data/", http.FileServer(http.Dir("data"))))
// 会话
http.HandleFunc("/session/ws", handlerWrapper(session.WSHandler))
http.HandleFunc("/session/save", handlerWrapper(session.SaveContent))
// 运行相关
http.HandleFunc("/build", handlerWrapper(output.BuildHandler))
http.HandleFunc("/run", handlerWrapper(output.RunHandler))
http.HandleFunc("/stop", handlerWrapper(output.StopHandler))
http.HandleFunc("/go/get", handlerWrapper(output.GoGetHandler))
http.HandleFunc("/go/install", handlerWrapper(output.GoInstallHandler))
http.HandleFunc("/output/ws", handlerWrapper(output.WSHandler))
// 文件树
http.HandleFunc("/files", handlerWrapper(file.GetFiles))
http.HandleFunc("/file", handlerWrapper(file.GetFile))
http.HandleFunc("/file/save", handlerWrapper(file.SaveFile))
http.HandleFunc("/file/new", handlerWrapper(file.NewFile))
http.HandleFunc("/file/remove", handlerWrapper(file.RemoveFile))
http.HandleFunc("/file/search/text", handlerWrapper(file.SearchText))
// 编辑器
http.HandleFunc("/editor/ws", handlerWrapper(editor.WSHandler))
http.HandleFunc("/go/fmt", handlerWrapper(editor.GoFmtHandler))
http.HandleFunc("/autocomplete", handlerWrapper(editor.AutocompleteHandler))
http.HandleFunc("/exprinfo", handlerWrapper(editor.GetExprInfoHandler))
http.HandleFunc("/find/decl", handlerWrapper(editor.FindDeclarationHandler))
http.HandleFunc("/find/usages", handlerWrapper(editor.FindUsagesHandler))
http.HandleFunc("/html/fmt", handlerWrapper(editor.HTMLFmtHandler))
http.HandleFunc("/json/fmt", handlerWrapper(editor.JSONFmtHandler))
// Shell
http.HandleFunc("/shell/ws", handlerWrapper(shell.WSHandler))
http.HandleFunc("/shell", handlerWrapper(shell.IndexHandler))
// 通知
http.HandleFunc("/notification/ws", handlerWrapper(notification.WSHandler))
// 用户
http.HandleFunc("/user/new", handlerWrapper(session.AddUser))
http.HandleFunc("/user/repos/init", handlerWrapper(session.InitGitRepos))
// 文档
http.Handle("/doc/", http.StripPrefix("/doc/", http.FileServer(http.Dir("doc"))))
glog.V(0).Infof("Wide is running [%s]", conf.Wide.Server)
err := http.ListenAndServe(conf.Wide.Server, nil)
if err != nil {
glog.Fatal(err)
}
}
// HTTP Handler 包装,完成共性处理.
//
// 共性处理:
// //
// 1. panic recover // 1. panic recover
// 2. request stopwatch // 2. 请求计时
// 3. i18n
func handlerWrapper(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { func handlerWrapper(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
handler := panicRecover(f) handler := panicRecover(f)
handler = stopwatch(handler) handler = stopwatch(handler)
handler = i18nLoad(handler)
return handler return handler
} }
// handlerGzWrapper wraps the HTTP Handler for some common processes. // Handler 包装请求计时.
//
// 1. panic recover
// 2. gzip response
// 3. request stopwatch
// 4. i18n
func handlerGzWrapper(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
handler := panicRecover(f)
handler = gzipWrapper(handler)
handler = stopwatch(handler)
handler = i18nLoad(handler)
return handler
}
// gzipWrapper wraps the process with response gzip.
func gzipWrapper(f func(http.ResponseWriter, *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
f(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
gzr := gzipResponseWriter{Writer: gz, ResponseWriter: w}
f(gzr, r)
}
}
// i18nLoad wraps the i18n process.
func i18nLoad(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
i18n.Load()
handler(w, r)
}
}
// stopwatch wraps the request stopwatch process.
func stopwatch(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { func stopwatch(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
start := time.Now() start := time.Now()
defer func() { defer func() {
logger.Tracef("[%s, %s, %s]", r.Method, r.RequestURI, time.Since(start)) glog.V(5).Infof("[%s] [%s]", r.RequestURI, time.Since(start))
}() }()
// Handler 处理
handler(w, r) handler(w, r)
} }
} }
// panicRecover wraps the panic recover process. // Handler 包装 recover panic.
func panicRecover(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { func panicRecover(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
defer gulu.Panic.Recover(nil) defer util.Recover()
// Handler 处理
handler(w, r) handler(w, r)
} }
} }
// initMime initializes mime types.
//
// We can't get the mime types on some OS (such as Windows XP) by default, so initializes them here.
func initMime() {
mime.AddExtensionType(".css", "text/css")
mime.AddExtensionType(".js", "application/x-javascript")
mime.AddExtensionType(".json", "application/json")
}
// gzipResponseWriter represents a gzip response writer.
type gzipResponseWriter struct {
io.Writer
http.ResponseWriter
}
// Write writes response with appropriate 'Content-Type'.
func (w gzipResponseWriter) Write(b []byte) (int, error) {
if "" == w.Header().Get("Content-Type") {
// If no content type, apply sniffing algorithm to un-gzipped body.
w.Header().Set("Content-Type", http.DetectContentType(b))
}
return w.Writer.Write(b)
}

View File

@ -1,48 +1,30 @@
// Copyright (c) 2014-present, b3log.org // 通知.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package notification includes notification related manipulations.
package notification package notification
import ( import (
"net/http" "net/http"
"os"
"strconv"
"time" "time"
"github.com/88250/gulu" "strconv"
"github.com/88250/wide/conf" "github.com/b3log/wide/conf"
"github.com/88250/wide/event" "github.com/b3log/wide/event"
"github.com/88250/wide/i18n" "github.com/b3log/wide/i18n"
"github.com/88250/wide/session" "github.com/b3log/wide/session"
"github.com/88250/wide/util" "github.com/b3log/wide/util"
"github.com/golang/glog"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
const ( const (
error = "ERROR" // notification.severity: ERROR Error = "ERROR" // 通知.严重程度:ERROR
warn = "WARN" // notification.severity: WARN Warn = "WARN" // 通知.严重程度:WARN
info = "INFO" // notification.severity: INFO Info = "INFO" // 通知.严重程度:INFO
setup = "Setup" // notification.type: setup Setup = "Setup" // 通知.类型:安装
server = "Server" // notification.type: server
) )
// Logger. // 通知结构.
var logger = gulu.Log.NewLogger(os.Stdout)
// Notification represents a notification.
type Notification struct { type Notification struct {
event *event.Event event *event.Event
Type string `json:"type"` Type string `json:"type"`
@ -50,74 +32,75 @@ type Notification struct {
Message string `json:"message"` Message string `json:"message"`
} }
// event2Notification processes user event by converting the specified event to a notification, and then push it to front // 用户事件处理:将事件转为通知,并通过通知通道推送给前端.
// browser with notification channel. //
// 当用户事件队列接收到事件时将会调用该函数进行处理.
func event2Notification(e *event.Event) { func event2Notification(e *event.Event) {
if nil == session.NotificationWS[e.Sid] { if nil == session.NotificationWS[e.Sid] {
return return
} }
wsChannel := session.NotificationWS[e.Sid] wsChannel := session.NotificationWS[e.Sid]
if nil == wsChannel {
return
}
httpSession, _ := session.HTTPSession.Get(wsChannel.Request, session.CookieName) var notification Notification
uid := httpSession.Values["uid"].(string)
locale := conf.GetUser(uid).Locale
var notification *Notification
switch e.Code { switch e.Code {
case event.EvtCodeGocodeNotFound: case event.EvtCodeGocodeNotFound:
fallthrough notification = Notification{event: e, Type: Setup, Severity: Error}
case event.EvtCodeIDEStubNotFound: case event.EvtCodeIDEStubNotFound:
notification = &Notification{event: e, Type: setup, Severity: error, notification = Notification{event: e, Type: Setup, Severity: Error}
Message: i18n.Get(locale, "notification_"+strconv.Itoa(e.Code)).(string)}
case event.EvtCodeServerInternalError:
notification = &Notification{event: e, Type: server, Severity: error,
Message: i18n.Get(locale, "notification_"+strconv.Itoa(e.Code)).(string) + " [" + e.Data.(string) + "]"}
default: default:
logger.Warnf("Can't handle event[code=%d]", e.Code) glog.Warningf("Can't handle event[code=%d]", e.Code)
return return
} }
wsChannel.WriteJSON(notification) httpSession, _ := session.HTTPSession.Get(wsChannel.Request, "wide-session")
username := httpSession.Values["username"].(string)
locale := conf.Wide.GetUser(username).Locale
wsChannel.Refresh() // 消息国际化处理
notification.Message = i18n.Get(locale, "notification_"+strconv.Itoa(e.Code)).(string)
wsChannel.Conn.WriteJSON(&notification)
// 更新通道最近使用时间
wsChannel.Time = time.Now()
} }
// WSHandler handles request of creating notification channel. // 建立通知通道.
func WSHandler(w http.ResponseWriter, r *http.Request) { func WSHandler(w http.ResponseWriter, r *http.Request) {
sid := r.URL.Query()["sid"][0] sid := r.URL.Query()["sid"][0]
wSession := session.WideSessions.Get(sid) wSession := session.WideSessions.Get(sid)
if nil == wSession { if nil == wSession {
glog.Errorf("Session [%s] not found", sid)
return return
} }
conn, _ := websocket.Upgrade(w, r, nil, 1024, 1024) conn, _ := websocket.Upgrade(w, r, nil, 1024, 1024)
wsChan := util.WSChannel{Sid: sid, Conn: conn, Request: r, Time: time.Now()} wsChan := util.WSChannel{Sid: sid, Conn: conn, Request: r, Time: time.Now()}
ret := map[string]interface{}{"notification": "Notification initialized", "cmd": "init-notification"}
err := wsChan.WriteJSON(&ret)
if nil != err {
return
}
session.NotificationWS[sid] = &wsChan session.NotificationWS[sid] = &wsChan
logger.Tracef("Open a new [Notification] with session [%s], %d", sid, len(session.NotificationWS)) glog.V(4).Infof("Open a new [Notification] with session [%s], %d", sid, len(session.NotificationWS))
// add user event handler // 添加用户事件处理器
wSession.EventQueue.AddHandler(event.HandleFunc(event2Notification)) wSession.EventQueue.AddHandler(event.HandleFunc(event2Notification))
input := map[string]interface{}{} input := map[string]interface{}{}
for { for {
if err := wsChan.ReadJSON(&input); err != nil { if err := wsChan.Conn.ReadJSON(&input); err != nil {
if err.Error() == "EOF" {
return
}
if err.Error() == "unexpected EOF" {
return
}
glog.Error("Notification WS ERROR: " + err.Error())
return return
} }
} }

View File

@ -1,315 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package output
import (
"bufio"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/88250/gulu"
"github.com/88250/wide/conf"
"github.com/88250/wide/i18n"
"github.com/88250/wide/session"
)
// BuildHandler handles request of building.
func BuildHandler(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult()
defer gulu.Ret.RetResult(w, r, result)
if conf.Wide.ReadOnly {
result.Code = -1
result.Msg = "readonly mode"
return
}
httpSession, _ := session.HTTPSession.Get(r, session.CookieName)
if httpSession.IsNew {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
uid := httpSession.Values["uid"].(string)
user := conf.GetUser(uid)
locale := user.Locale
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
result.Code = -1
return
}
sid := args["sid"].(string)
filePath := args["file"].(string)
if gulu.Go.IsAPI(filePath) || !session.CanAccess(uid, filePath) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
curDir := filepath.Dir(filePath)
fout, err := os.Create(filePath)
if nil != err {
logger.Error(err)
result.Code = -1
return
}
code := args["code"].(string)
if _, err := fout.WriteString(code); nil != err {
logger.Error(err)
result.Code = -1
return
}
fout.Close()
channelRet := map[string]interface{}{}
if nil != session.OutputWS[sid] {
// display "START [go build]" in front-end browser
msg := i18n.Get(locale, "start-build").(string)
msg = strings.Replace(msg, "build]", "build "+fmt.Sprint(user.BuildArgs(runtime.GOOS))+"]", 1)
channelRet["output"] = "<span class='start-build'>" + msg + "</span>\n"
channelRet["cmd"] = "start-build"
wsChannel := session.OutputWS[sid]
wsChannel.WriteJSON(&channelRet)
wsChannel.Refresh()
}
var goModCmd *exec.Cmd
if !gulu.File.IsExist(filepath.Join(curDir, "go.mod")) {
curDirName := filepath.Base(curDir)
goModCmd = exec.Command("go", "mod", "init", curDirName)
} else {
goModCmd = exec.Command("go", "mod", "tidy")
}
goModCmd.Dir = curDir
setCmdEnv(goModCmd, uid)
outputBytes, err := goModCmd.CombinedOutput()
output := string(outputBytes)
if nil != err && strings.Contains(output, "go.mod already exists") {
logger.Error(err.Error() + ": " + output)
result.Code = -1
return
}
var goBuildArgs []string
goBuildArgs = append(goBuildArgs, "build")
goBuildArgs = append(goBuildArgs, user.BuildArgs(runtime.GOOS)...)
//if !gulu.Str.Contains("-i", goBuildArgs) {
// goBuildArgs = append(goBuildArgs, "-i")
//}
cmd := exec.Command("go", goBuildArgs...)
cmd.Dir = curDir
setCmdEnv(cmd, uid)
suffix := ""
if gulu.OS.IsWindows() {
suffix = ".exe"
}
executable := filepath.Base(curDir) + suffix
executable = filepath.Join(curDir, executable)
stdout, err := cmd.StdoutPipe()
if nil != err {
logger.Error(err)
result.Code = -1
return
}
stderr, err := cmd.StderrPipe()
if nil != err {
logger.Error(err)
result.Code = -1
return
}
if 0 != result.Code {
return
}
if err := cmd.Start(); nil != err {
logger.Error(err)
result.Code = -1
return
}
channelRet["cmd"] = "build"
channelRet["executable"] = executable
outReader := bufio.NewReader(stdout)
/////////
go func() {
defer gulu.Panic.Recover(nil)
for {
wsChannel := session.OutputWS[sid]
if nil == wsChannel {
break
}
line, err := outReader.ReadString('\n')
if io.EOF == err {
break
}
_, ok := err.(*os.PathError)
if ok {
// 构建时报 “read |0: file already closed” https://github.com/b3log/wide/issues/363
break
}
if nil != err {
logger.Warnf("%#v", err)
break
}
channelRet["output"] = line
err = wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Warn(err)
break
}
wsChannel.Refresh()
}
}()
errReader := bufio.NewReader(stderr)
var lines []string
for {
wsChannel := session.OutputWS[sid]
if nil == wsChannel {
break
}
line, err := errReader.ReadString('\n')
if io.EOF == err {
break
}
lines = append(lines, line)
if nil != err {
logger.Warn(err)
break
}
// path process
errOutWithPath := parsePath(curDir, line)
channelRet["output"] = "<span class='stderr'>" + errOutWithPath + "</span>"
err = wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Warn(err)
break
}
wsChannel.Refresh()
}
if nil == cmd.Wait() {
channelRet["nextCmd"] = args["nextCmd"]
channelRet["output"] = "<span class='build-succ'>" + i18n.Get(locale, "build-succ").(string) + "</span>\n"
} else {
channelRet["output"] = "<span class='build-error'>" + i18n.Get(locale, "build-error").(string) + "</span>\n"
// lint process
if lines[0][0] == '#' {
lines = lines[1:] // skip the first line
}
lints := []*Lint{}
for _, line := range lines {
if len(line) < 1 || !strings.Contains(line, ":") {
continue
}
if line[0] == '\t' {
// append to the last lint
last := len(lints)
msg := lints[last-1].Msg
msg += line
lints[last-1].Msg = msg
continue
}
file := line[:strings.Index(line, ":")]
left := line[strings.Index(line, ":")+1:]
index := strings.Index(left, ":")
lineNo := 0
msg := left
if index >= 0 {
lineNo, err = strconv.Atoi(left[:index])
if nil != err {
continue
}
msg = left[index+2:]
}
lint := &Lint{
File: filepath.ToSlash(filepath.Join(curDir, file)),
LineNo: lineNo - 1,
Severity: lintSeverityError,
Msg: msg,
}
lints = append(lints, lint)
}
channelRet["lints"] = lints
}
wsChannel := session.OutputWS[sid]
if nil == wsChannel {
return
}
err = wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Warn(err)
}
wsChannel.Refresh()
}

View File

@ -1,252 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package output
import (
"bufio"
"encoding/json"
"io"
"io/ioutil"
"math/rand"
"net/http"
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/88250/gulu"
"github.com/88250/wide/conf"
"github.com/88250/wide/i18n"
"github.com/88250/wide/session"
)
// CrossCompilationHandler handles request of cross compilation.
func CrossCompilationHandler(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult()
defer gulu.Ret.RetResult(w, r, result)
if conf.Wide.ReadOnly {
result.Code = -1
result.Msg = "readonly mode"
return
}
httpSession, _ := session.HTTPSession.Get(r, session.CookieName)
if httpSession.IsNew {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
uid := httpSession.Values["uid"].(string)
locale := conf.GetUser(uid).Locale
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
result.Code = -1
return
}
sid := args["sid"].(string)
filePath := args["path"].(string)
if gulu.Go.IsAPI(filePath) || !session.CanAccess(uid, filePath) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
platform := args["platform"].(string)
goos := strings.Split(platform, "_")[0]
goarch := strings.Split(platform, "_")[1]
curDir := filepath.Dir(filePath)
suffix := ""
if "windows" == goos {
suffix = ".exe"
}
user := conf.GetUser(uid)
goBuildArgs := []string{}
goBuildArgs = append(goBuildArgs, "build")
goBuildArgs = append(goBuildArgs, user.BuildArgs(goos)...)
cmd := exec.Command("go", goBuildArgs...)
cmd.Dir = curDir
setCmdEnv(cmd, uid)
for i, env := range cmd.Env {
if strings.HasPrefix(env, "GOOS=") {
cmd.Env[i] = "GOOS=" + goos
continue
}
if strings.HasPrefix(env, "GOARCH=") {
cmd.Env[i] = "GOARCH=" + goarch
continue
}
}
executable := filepath.Base(curDir) + suffix
executable = filepath.Join(curDir, executable)
name := filepath.Base(curDir) + "-" + goos + "-" + goarch
stdout, err := cmd.StdoutPipe()
if nil != err {
logger.Error(err)
result.Code = -1
return
}
stderr, err := cmd.StderrPipe()
if nil != err {
logger.Error(err)
result.Code = -1
return
}
if 0 != result.Code {
return
}
channelRet := map[string]interface{}{}
if nil != session.OutputWS[sid] {
// display "START [go build]" in front-end browser
channelRet["output"] = "<span class='start-build'>" + i18n.Get(locale, "start-build").(string) + "</span>\n"
channelRet["cmd"] = "start-build"
wsChannel := session.OutputWS[sid]
err := wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Error(err)
return
}
wsChannel.Refresh()
}
reader := bufio.NewReader(io.MultiReader(stdout, stderr))
if err := cmd.Start(); nil != err {
logger.Error(err)
result.Code = -1
return
}
go func(runningId int) {
defer gulu.Panic.Recover(nil)
defer cmd.Wait()
// read all
buf, _ := ioutil.ReadAll(reader)
channelRet := map[string]interface{}{}
channelRet["cmd"] = "cross-build"
channelRet["executable"] = executable
channelRet["name"] = name
if 0 == len(buf) { // build success
channelRet["output"] = "<span class='build-succ'>" + i18n.Get(locale, "build-succ").(string) + "</span>\n"
} else { // build error
// build gutter lint
errOut := string(buf)
lines := strings.Split(errOut, "\n")
// path process
var errOutWithPath string
for _, line := range lines {
errOutWithPath += parsePath(curDir, line) + "\n"
}
channelRet["output"] = "<span class='build-error'>" + i18n.Get(locale, "build-error").(string) + "</span>\n" +
"<span class='stderr'>" + errOutWithPath + "</span>"
// lint process
if lines[0][0] == '#' {
lines = lines[1:] // skip the first line
}
lints := []*Lint{}
for _, line := range lines {
if len(line) < 1 {
continue
}
if line[0] == '\t' {
// append to the last lint
last := len(lints)
msg := lints[last-1].Msg
msg += line
lints[last-1].Msg = msg
continue
}
file := line[:strings.Index(line, ":")]
left := line[strings.Index(line, ":")+1:]
index := strings.Index(left, ":")
lineNo := 0
msg := left
if index >= 0 {
lineNo, err = strconv.Atoi(left[:index])
if nil != err {
continue
}
msg = left[index+2:]
}
lint := &Lint{
File: filepath.Join(curDir, file),
LineNo: lineNo - 1,
Severity: lintSeverityError,
Msg: msg,
}
lints = append(lints, lint)
}
channelRet["lints"] = lints
}
if nil != session.OutputWS[sid] {
wsChannel := session.OutputWS[sid]
err := wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Warn(err)
}
wsChannel.Refresh()
}
}(rand.Int())
}

View File

@ -1,204 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package output
import (
"bufio"
"encoding/json"
"io"
"io/ioutil"
"math/rand"
"net/http"
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/88250/gulu"
"github.com/88250/wide/conf"
"github.com/88250/wide/i18n"
"github.com/88250/wide/session"
)
// GoInstallHandler handles request of go install.
func GoInstallHandler(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult()
defer gulu.Ret.RetResult(w, r, result)
if conf.Wide.ReadOnly {
result.Code = -1
result.Msg = "readonly mode"
return
}
httpSession, _ := session.HTTPSession.Get(r, session.CookieName)
if httpSession.IsNew {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
uid := httpSession.Values["uid"].(string)
locale := conf.GetUser(uid).Locale
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
result.Code = -1
return
}
sid := args["sid"].(string)
filePath := args["file"].(string)
curDir := filepath.Dir(filePath)
cmd := exec.Command("go", "install")
cmd.Dir = curDir
setCmdEnv(cmd, uid)
logger.Debugf("go install %s", curDir)
stdout, err := cmd.StdoutPipe()
if nil != err {
logger.Error(err)
result.Code = -1
return
}
stderr, err := cmd.StderrPipe()
if nil != err {
logger.Error(err)
result.Code = -1
return
}
if 0 != result.Code {
return
}
channelRet := map[string]interface{}{}
if nil != session.OutputWS[sid] {
// display "START [go install]" in front-end browser
channelRet["output"] = "<span class='start-install'>" + i18n.Get(locale, "start-install").(string) + "</span>\n"
channelRet["cmd"] = "start-install"
wsChannel := session.OutputWS[sid]
err := wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Error(err)
return
}
wsChannel.Refresh()
}
reader := bufio.NewReader(io.MultiReader(stdout, stderr))
if err := cmd.Start(); nil != err {
logger.Error(err)
result.Code = -1
return
}
go func(runningId int) {
defer gulu.Panic.Recover(nil)
defer cmd.Wait()
logger.Debugf("User [%s, %s] is running [go install] [id=%d, dir=%s]", uid, sid, runningId, curDir)
// read all
buf, _ := ioutil.ReadAll(reader)
channelRet := map[string]interface{}{}
channelRet["cmd"] = "go install"
if 0 != len(buf) { // build error
// build gutter lint
errOut := string(buf)
lines := strings.Split(errOut, "\n")
if lines[0][0] == '#' {
lines = lines[1:] // skip the first line
}
lints := []*Lint{}
for _, line := range lines {
if len(line) < 1 {
continue
}
if line[0] == '\t' {
// append to the last lint
last := len(lints)
msg := lints[last-1].Msg
msg += line
lints[last-1].Msg = msg
continue
}
file := line[:strings.Index(line, ":")]
left := line[strings.Index(line, ":")+1:]
index := strings.Index(left, ":")
lineNo := 0
msg := left
if index >= 0 {
lineNo, _ = strconv.Atoi(left[:index])
msg = left[index+2:]
}
lint := &Lint{
File: file,
LineNo: lineNo - 1,
Severity: lintSeverityError,
Msg: msg,
}
lints = append(lints, lint)
}
channelRet["lints"] = lints
channelRet["output"] = "<span class='install-error'>" + i18n.Get(locale, "install-error").(string) + "</span>\n" + errOut
} else {
channelRet["output"] = "<span class='install-succ'>" + i18n.Get(locale, "install-succ").(string) + "</span>\n"
}
if nil != session.OutputWS[sid] {
logger.Debugf("User [%s, %s] 's running [go install] [id=%d, dir=%s] has done", uid, sid, runningId, curDir)
wsChannel := session.OutputWS[sid]
err := wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Warn(err)
}
wsChannel.Refresh()
}
}(rand.Int())
}

View File

@ -1,132 +1,723 @@
// Copyright (c) 2014-present, b3log.org // 构建、运行、go tool 操作.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package output includes build, run and go tool related manipulations.
package output package output
import ( import (
"bufio"
"encoding/json"
"io"
"io/ioutil"
"math/rand"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/88250/gulu" "github.com/b3log/wide/conf"
"github.com/88250/wide/conf" "github.com/b3log/wide/i18n"
"github.com/88250/wide/session" "github.com/b3log/wide/session"
"github.com/88250/wide/util" "github.com/b3log/wide/util"
"github.com/golang/glog"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
const ( const (
lintSeverityError = "error" // lint severity: error lintSeverityError = "error" // Lint 严重级别:错误
lintSeverityWarn = "warning" // lint severity: warning lintSeverityWarn = "warning" // Lint 严重级别:警告
) )
// Logger. // 代码 Lint 结构.
var logger = gulu.Log.NewLogger(os.Stdout)
// Lint represents a code lint.
type Lint struct { type Lint struct {
File string `json:"file"` File string `json:"file"`
LineNo int `json:"lineNo"` LineNo int `json:"lineNo"`
Severity string `json:"severity"` Severity string `json:"severity"`
Msg string `json:"msg"` Msg string
} }
// WSHandler handles request of creating output channel. // 建立输出通道.
func WSHandler(w http.ResponseWriter, r *http.Request) { func WSHandler(w http.ResponseWriter, r *http.Request) {
sid := r.URL.Query()["sid"][0] sid := r.URL.Query()["sid"][0]
conn, _ := websocket.Upgrade(w, r, nil, 1024, 1024) conn, _ := websocket.Upgrade(w, r, nil, 1024, 1024)
wsChan := util.WSChannel{Sid: sid, Conn: conn, Request: r, Time: time.Now()} wsChan := util.WSChannel{Sid: sid, Conn: conn, Request: r, Time: time.Now()}
session.OutputWS[sid] = &wsChan
ret := map[string]interface{}{"output": "Ouput initialized", "cmd": "init-output"} ret := map[string]interface{}{"output": "Ouput initialized", "cmd": "init-output"}
err := wsChan.WriteJSON(&ret) wsChan.Conn.WriteJSON(&ret)
if nil != err {
glog.V(4).Infof("Open a new [Output] with session [%s], %d", sid, len(session.OutputWS))
}
// 运行一个可执行文件.
func RunHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{"succ": true}
defer util.RetJSON(w, r, data)
decoder := json.NewDecoder(r.Body)
var args map[string]interface{}
if err := decoder.Decode(&args); err != nil {
glog.Error(err)
data["succ"] = false
return return
} }
session.OutputWS[sid] = &wsChan sid := args["sid"].(string)
wSession := session.WideSessions.Get(sid)
if nil == wSession {
data["succ"] = false
logger.Tracef("Open a new [Output] with session [%s], %d", sid, len(session.OutputWS)) return
}
// parsePath parses file path in the specified outputLine, and returns new line with front-end friendly.
func parsePath(curDir, outputLine string) string {
index := strings.Index(outputLine, " ")
if -1 == index || index >= len(outputLine) {
return outputLine
} }
pathPart := outputLine[:index] filePath := args["executable"].(string)
msgPart := outputLine[index:] curDir := filePath[:strings.LastIndex(filePath, conf.PathSeparator)]
parts := strings.Split(pathPart, ":") cmd := exec.Command(filePath)
if len(parts) < 2 { // no file path info (line & column) found cmd.Dir = curDir
return outputLine
}
file := parts[0] stdout, err := cmd.StdoutPipe()
line := parts[1]
if _, err := strconv.Atoi(line); nil != err {
return outputLine
}
column := "0"
hasColumn := 4 == len(parts)
if hasColumn {
column = parts[2]
}
tagStart := `<span class="path" data-path="` + filepath.ToSlash(filepath.Join(curDir, file)) + `" data-line="` + line +
`" data-column="` + column + `">`
text := file + ":" + line
if hasColumn {
text += ":" + column
}
tagEnd := "</span>:"
return tagStart + text + tagEnd + msgPart
}
func setCmdEnv(cmd *exec.Cmd, uid string) {
userWorkspace := conf.GetUserWorkspace(uid)
cache, err := os.UserCacheDir()
if nil != err { if nil != err {
logger.Warnf("Get user cache dir failed [" + err.Error() + "]") glog.Error(err)
cache = os.TempDir() data["succ"] = false
return
} }
stderr, err := cmd.StderrPipe()
if nil != err {
glog.Error(err)
data["succ"] = false
return
}
reader := bufio.NewReader(io.MultiReader(stdout, stderr))
if err := cmd.Start(); nil != err {
glog.Error(err)
data["succ"] = false
return
}
// 添加到用户进程集中
processes.add(wSession, cmd.Process)
channelRet := map[string]interface{}{}
channelRet["pid"] = cmd.Process.Pid
go func(runningId int) {
defer util.Recover()
defer cmd.Wait()
glog.V(3).Infof("Session [%s] is running [id=%d, file=%s]", sid, runningId, filePath)
// 在读取程序输出前先返回一次,使前端获取到 run 状态与 pid
if nil != session.OutputWS[sid] {
wsChannel := session.OutputWS[sid]
channelRet["cmd"] = "run"
channelRet["output"] = ""
err := wsChannel.Conn.WriteJSON(&channelRet)
if nil != err {
glog.Error(err)
return
}
// 更新通道最近使用时间
wsChannel.Time = time.Now()
}
for {
buf, err := reader.ReadBytes('\n')
if nil != err || 0 == len(buf) {
// 从用户进程集中移除这个执行完毕(或是被主动停止)的进程
processes.remove(wSession, cmd.Process)
glog.V(3).Infof("Session [%s] 's running [id=%d, file=%s] has done", sid, runningId, filePath)
if nil != session.OutputWS[sid] {
wsChannel := session.OutputWS[sid]
channelRet["cmd"] = "run-done"
channelRet["output"] = "<pre>" + string(buf) + "</pre>"
err := wsChannel.Conn.WriteJSON(&channelRet)
if nil != err {
glog.Error(err)
break
}
// 更新通道最近使用时间
wsChannel.Time = time.Now()
}
break
} else {
if nil != session.OutputWS[sid] {
wsChannel := session.OutputWS[sid]
channelRet["cmd"] = "run"
channelRet["output"] = "<pre>" + string(buf) + "</pre>"
err := wsChannel.Conn.WriteJSON(&channelRet)
if nil != err {
glog.Error(err)
break
}
// 更新通道最近使用时间
wsChannel.Time = time.Now()
}
}
}
}(rand.Int())
}
// 构建可执行文件.
func BuildHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{"succ": true}
defer util.RetJSON(w, r, data)
httpSession, _ := session.HTTPSession.Get(r, "wide-session")
username := httpSession.Values["username"].(string)
locale := conf.Wide.GetUser(username).Locale
decoder := json.NewDecoder(r.Body)
var args map[string]interface{}
if err := decoder.Decode(&args); err != nil {
glog.Error(err)
data["succ"] = false
return
}
sid := args["sid"].(string)
filePath := args["file"].(string)
curDir := filePath[:strings.LastIndex(filePath, conf.PathSeparator)]
fout, err := os.Create(filePath)
if nil != err {
glog.Error(err)
data["succ"] = false
return
}
code := args["code"].(string)
fout.WriteString(code)
if err := fout.Close(); nil != err {
glog.Error(err)
data["succ"] = false
return
}
suffix := ""
if "windows" == runtime.GOOS {
suffix = ".exe"
}
executable := "main" + suffix
argv := []string{"build", "-o", executable}
cmd := exec.Command("go", argv...)
cmd.Dir = curDir
setCmdEnv(cmd, username)
glog.V(5).Infof("go build -o %s", executable)
executable = curDir + conf.PathSeparator + executable
// 先把可执行文件删了
err = os.RemoveAll(executable)
if nil != err {
glog.Info(err)
data["succ"] = false
return
}
stdout, err := cmd.StdoutPipe()
if nil != err {
glog.Error(err)
data["succ"] = false
return
}
stderr, err := cmd.StderrPipe()
if nil != err {
glog.Error(err)
data["succ"] = false
return
}
if !data["succ"].(bool) {
return
}
channelRet := map[string]interface{}{}
if nil != session.OutputWS[sid] {
// 在前端 output 中显示“开始构建”
channelRet["output"] = "<span class='start-build'>" + i18n.Get(locale, "start-build").(string) + "</span>\n"
channelRet["cmd"] = "start-build"
wsChannel := session.OutputWS[sid]
err := wsChannel.Conn.WriteJSON(&channelRet)
if nil != err {
glog.Error(err)
return
}
// 更新通道最近使用时间
wsChannel.Time = time.Now()
}
reader := bufio.NewReader(io.MultiReader(stdout, stderr))
if err := cmd.Start(); nil != err {
glog.Error(err)
data["succ"] = false
return
}
go func(runningId int) {
defer util.Recover()
defer cmd.Wait()
glog.V(3).Infof("Session [%s] is building [id=%d, dir=%s]", sid, runningId, curDir)
// 一次性读取
buf, _ := ioutil.ReadAll(reader)
channelRet := map[string]interface{}{}
channelRet["cmd"] = "build"
channelRet["executable"] = executable
if 0 == len(buf) { // 说明构建成功,没有错误信息输出
// 设置下一次执行命令(前端会根据该参数发送请求)
channelRet["nextCmd"] = args["nextCmd"]
channelRet["output"] = "<span class='build-succ'>" + i18n.Get(locale, "build-succ").(string) + "</span>\n"
go func() { // 运行 go install生成的库用于 gocode lib-path
cmd := exec.Command("go", "install")
cmd.Dir = curDir
setCmdEnv(cmd, username)
out, _ := cmd.CombinedOutput()
if len(out) > 0 {
glog.Warning(string(out))
}
}()
} else { // 构建失败
// 解析错误信息,返回给编辑器 gutter lint
errOut := string(buf)
channelRet["output"] = "<span class='build-failed'>" + i18n.Get(locale, "build-failed").(string) + "</span>\n" + errOut
lines := strings.Split(errOut, "\n")
if lines[0][0] == '#' {
lines = lines[1:] // 跳过第一行
}
lints := []*Lint{}
for _, line := range lines {
if len(line) < 1 {
continue
}
if line[0] == '\t' {
// 添加到上一个 lint 中
last := len(lints)
msg := lints[last-1].Msg
msg += line
lints[last-1].Msg = msg
continue
}
file := line[:strings.Index(line, ":")]
left := line[strings.Index(line, ":")+1:]
lineNo, _ := strconv.Atoi(left[:strings.Index(left, ":")])
msg := left[strings.Index(left, ":")+2:]
lint := &Lint{
File: file,
LineNo: lineNo - 1,
Severity: lintSeverityError,
Msg: msg,
}
lints = append(lints, lint)
}
channelRet["lints"] = lints
}
if nil != session.OutputWS[sid] {
glog.V(3).Infof("Session [%s] 's build [id=%d, dir=%s] has done", sid, runningId, curDir)
wsChannel := session.OutputWS[sid]
err := wsChannel.Conn.WriteJSON(&channelRet)
if nil != err {
glog.Error(err)
}
// 更新通道最近使用时间
wsChannel.Time = time.Now()
}
}(rand.Int())
}
// go install.
func GoInstallHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{"succ": true}
defer util.RetJSON(w, r, data)
httpSession, _ := session.HTTPSession.Get(r, "wide-session")
username := httpSession.Values["username"].(string)
locale := conf.Wide.GetUser(username).Locale
decoder := json.NewDecoder(r.Body)
var args map[string]interface{}
if err := decoder.Decode(&args); err != nil {
glog.Error(err)
data["succ"] = false
return
}
sid := args["sid"].(string)
filePath := args["file"].(string)
curDir := filePath[:strings.LastIndex(filePath, conf.PathSeparator)]
fout, err := os.Create(filePath)
if nil != err {
glog.Error(err)
data["succ"] = false
return
}
code := args["code"].(string)
fout.WriteString(code)
if err := fout.Close(); nil != err {
glog.Error(err)
data["succ"] = false
return
}
cmd := exec.Command("go", "install")
cmd.Dir = curDir
setCmdEnv(cmd, username)
glog.V(5).Infof("go install %s", curDir)
stdout, err := cmd.StdoutPipe()
if nil != err {
glog.Error(err)
data["succ"] = false
return
}
stderr, err := cmd.StderrPipe()
if nil != err {
glog.Error(err)
data["succ"] = false
return
}
if !data["succ"].(bool) {
return
}
channelRet := map[string]interface{}{}
if nil != session.OutputWS[sid] {
// 在前端 output 中显示“开始 go install”
channelRet["output"] = "<span class='start-install'>" + i18n.Get(locale, "start-install").(string) + "</span>\n"
channelRet["cmd"] = "start-install"
wsChannel := session.OutputWS[sid]
err := wsChannel.Conn.WriteJSON(&channelRet)
if nil != err {
glog.Error(err)
return
}
// 更新通道最近使用时间
wsChannel.Time = time.Now()
}
reader := bufio.NewReader(io.MultiReader(stdout, stderr))
if err := cmd.Start(); nil != err {
glog.Error(err)
data["succ"] = false
return
}
go func(runningId int) {
defer util.Recover()
defer cmd.Wait()
glog.V(3).Infof("Session [%s] is running [go install] [id=%d, dir=%s]", sid, runningId, curDir)
// 一次性读取
buf, _ := ioutil.ReadAll(reader)
channelRet := map[string]interface{}{}
channelRet["cmd"] = "go install"
if 0 != len(buf) { // 构建失败
// 解析错误信息,返回给编辑器 gutter lint
errOut := string(buf)
lines := strings.Split(errOut, "\n")
if lines[0][0] == '#' {
lines = lines[1:] // 跳过第一行
}
lints := []*Lint{}
for _, line := range lines {
if len(line) < 1 {
continue
}
if line[0] == '\t' {
// 添加到上一个 lint 中
last := len(lints)
msg := lints[last-1].Msg
msg += line
lints[last-1].Msg = msg
continue
}
file := line[:strings.Index(line, ":")]
left := line[strings.Index(line, ":")+1:]
lineNo, _ := strconv.Atoi(left[:strings.Index(left, ":")])
msg := left[strings.Index(left, ":")+2:]
lint := &Lint{
File: file,
LineNo: lineNo - 1,
Severity: lintSeverityError,
Msg: msg,
}
lints = append(lints, lint)
}
channelRet["lints"] = lints
channelRet["output"] = "<span class='install-failed'>" + i18n.Get(locale, "install-failed").(string) + "</span>\n" + errOut
} else {
channelRet["output"] = "<span class='install-succ'>" + i18n.Get(locale, "install-succ").(string) + "</span>\n"
}
if nil != session.OutputWS[sid] {
glog.V(3).Infof("Session [%s] 's running [go install] [id=%d, dir=%s] has done", sid, runningId, curDir)
wsChannel := session.OutputWS[sid]
err := wsChannel.Conn.WriteJSON(&channelRet)
if nil != err {
glog.Error(err)
}
// 更新通道最近使用时间
wsChannel.Time = time.Now()
}
}(rand.Int())
}
// go get.
func GoGetHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{"succ": true}
defer util.RetJSON(w, r, data)
httpSession, _ := session.HTTPSession.Get(r, "wide-session")
username := httpSession.Values["username"].(string)
locale := conf.Wide.GetUser(username).Locale
decoder := json.NewDecoder(r.Body)
var args map[string]interface{}
if err := decoder.Decode(&args); err != nil {
glog.Error(err)
data["succ"] = false
return
}
sid := args["sid"].(string)
filePath := args["file"].(string)
curDir := filePath[:strings.LastIndex(filePath, conf.PathSeparator)]
cmd := exec.Command("go", "get")
cmd.Dir = curDir
setCmdEnv(cmd, username)
stdout, err := cmd.StdoutPipe()
if nil != err {
glog.Error(err)
data["succ"] = false
return
}
stderr, err := cmd.StderrPipe()
if nil != err {
glog.Error(err)
data["succ"] = false
return
}
if !data["succ"].(bool) {
return
}
channelRet := map[string]interface{}{}
if nil != session.OutputWS[sid] {
// 在前端 output 中显示“开始 go get
channelRet["output"] = "<span class='start-get'>" + i18n.Get(locale, "start-get").(string) + "</span>\n"
channelRet["cmd"] = "start-get"
wsChannel := session.OutputWS[sid]
err := wsChannel.Conn.WriteJSON(&channelRet)
if nil != err {
glog.Error(err)
return
}
// 更新通道最近使用时间
wsChannel.Time = time.Now()
}
reader := bufio.NewReader(io.MultiReader(stdout, stderr))
if err := cmd.Start(); nil != err {
glog.Error(err)
data["succ"] = false
return
}
go func(runningId int) {
defer util.Recover()
defer cmd.Wait()
glog.V(3).Infof("Session [%s] is running [go get] [runningId=%d]", sid, runningId)
channelRet := map[string]interface{}{}
channelRet["cmd"] = "go get"
// 一次性读取
buf, _ := ioutil.ReadAll(reader)
if 0 != len(buf) {
glog.V(3).Infof("Session [%s] 's running [go get] [runningId=%d] has done (with error)", sid, runningId)
channelRet["output"] = "<span class='get-failed'>" + i18n.Get(locale, "get-failed").(string) + "</span>\n" + string(buf)
} else {
glog.V(3).Infof("Session [%s] 's running [go get] [runningId=%d] has done", sid, runningId)
channelRet["output"] = "<span class='get-succ'>" + i18n.Get(locale, "get-succ").(string) + "</span>\n"
}
if nil != session.OutputWS[sid] {
wsChannel := session.OutputWS[sid]
err := wsChannel.Conn.WriteJSON(&channelRet)
if nil != err {
glog.Error(err)
}
// 更新通道最近使用时间
wsChannel.Time = time.Now()
}
}(rand.Int())
}
// 结束正在运行的进程.
func StopHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{"succ": true}
defer util.RetJSON(w, r, data)
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
glog.Error(err)
data["succ"] = false
return
}
sid := args["sid"].(string)
pid := int(args["pid"].(float64))
wSession := session.WideSessions.Get(sid)
if nil == wSession {
data["succ"] = false
return
}
processes.kill(wSession, pid)
}
func setCmdEnv(cmd *exec.Cmd, username string) {
userWorkspace := conf.Wide.GetUserWorkspace(username)
masterWorkspace := conf.Wide.GetWorkspace()
cmd.Env = append(cmd.Env, cmd.Env = append(cmd.Env,
"GOPROXY=https://goproxy.cn", "GOPATH="+userWorkspace+conf.PathListSeparator+masterWorkspace,
"GO111MODULE=on",
"GOPATH="+userWorkspace,
"GOOS="+runtime.GOOS, "GOOS="+runtime.GOOS,
"GOARCH="+runtime.GOARCH, "GOARCH="+runtime.GOARCH,
"GOROOT="+runtime.GOROOT(), "GOROOT="+runtime.GOROOT(),
"GOCACHE="+cache,
"PATH="+os.Getenv("PATH")) "PATH="+os.Getenv("PATH"))
if gulu.OS.IsWindows() {
// FIXME: for some weird issues on Windows, such as: The requested service provider could not be loaded or initialized.
cmd.Env = append(cmd.Env, os.Environ()...)
} else {
// 编译链接时找不到依赖的动态库 https://github.com/b3log/wide/issues/352
cmd.Env = append(cmd.Env, "LD_LIBRARY_PATH="+os.Getenv("LD_LIBRARY_PATH"))
}
} }

92
output/processes.go Normal file
View File

@ -0,0 +1,92 @@
package output
import (
"os"
"sync"
"github.com/b3log/wide/session"
"github.com/golang/glog"
)
// 进程集类型.
type procs map[string][]*os.Process
// 所有用户正在运行的程序进程集.
//
// <sid, []*os.Process>
var processes = procs{}
// 排它锁,防止并发修改.
var mutex sync.Mutex
// 添加用户执行进程.
func (procs *procs) add(wSession *session.WideSession, proc *os.Process) {
mutex.Lock()
defer mutex.Unlock()
sid := wSession.Id
userProcesses := (*procs)[sid]
userProcesses = append(userProcesses, proc)
(*procs)[sid] = userProcesses
// 会话关联进程
wSession.SetProcesses(userProcesses)
glog.V(3).Infof("Session [%s] has [%d] processes", sid, len((*procs)[sid]))
}
// 移除用户执行进程.
func (procs *procs) remove(wSession *session.WideSession, proc *os.Process) {
mutex.Lock()
defer mutex.Unlock()
sid := wSession.Id
userProcesses := (*procs)[sid]
var newProcesses []*os.Process
for i, p := range userProcesses {
if p.Pid == proc.Pid {
newProcesses = append(userProcesses[:i], userProcesses[i+1:]...)
(*procs)[sid] = newProcesses
// 会话关联进程
wSession.SetProcesses(newProcesses)
glog.V(3).Infof("Session [%s] has [%d] processes", sid, len((*procs)[sid]))
return
}
}
}
// 结束用户正在执行的进程.
func (procs *procs) kill(wSession *session.WideSession, pid int) {
mutex.Lock()
defer mutex.Unlock()
sid := wSession.Id
userProcesses := (*procs)[sid]
for i, p := range userProcesses {
if p.Pid == pid {
if err := p.Kill(); nil != err {
glog.Error("Kill a process [pid=%d] of session [%s] failed [error=%v]", pid, sid, err)
} else {
var newProcesses []*os.Process
newProcesses = append(userProcesses[:i], userProcesses[i+1:]...)
(*procs)[sid] = newProcesses
// 会话关联进程
wSession.SetProcesses(newProcesses)
glog.V(3).Infof("Killed a process [pid=%d] of session [%s]", pid, sid)
}
return
}
}
}

View File

@ -1,30 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package output
import (
"github.com/88250/wide/session"
"net/http"
)
// RunHandler handles request of executing a binary file.
func RunHandler(w http.ResponseWriter, r *http.Request) {
session.RunHandler(w, r, session.OutputWS)
}
// StopHandler handles request of stopping a running process.
func StopHandler(w http.ResponseWriter, r *http.Request) {
session.StopHandler(w, r)
}

View File

@ -1,149 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package output
import (
"bufio"
"encoding/json"
"io"
"io/ioutil"
"math/rand"
"net/http"
"os/exec"
"path/filepath"
"github.com/88250/gulu"
"github.com/88250/wide/conf"
"github.com/88250/wide/i18n"
"github.com/88250/wide/session"
)
// GoTestHandler handles request of go test.
func GoTestHandler(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult()
defer gulu.Ret.RetResult(w, r, result)
httpSession, _ := session.HTTPSession.Get(r, session.CookieName)
if httpSession.IsNew {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
uid := httpSession.Values["uid"].(string)
locale := conf.GetUser(uid).Locale
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
result.Code = -1
return
}
sid := args["sid"].(string)
filePath := args["file"].(string)
curDir := filepath.Dir(filePath)
cmd := exec.Command("go", "test", "-v")
cmd.Dir = curDir
setCmdEnv(cmd, uid)
stdout, err := cmd.StdoutPipe()
if nil != err {
logger.Error(err)
result.Code = -1
return
}
stderr, err := cmd.StderrPipe()
if nil != err {
logger.Error(err)
result.Code = -1
return
}
if 0 != result.Code {
return
}
channelRet := map[string]interface{}{}
if nil != session.OutputWS[sid] {
// display "START [go test]" in front-end browser
channelRet["output"] = "<span class='start-test'>" + i18n.Get(locale, "start-test").(string) + "</span>\n"
channelRet["cmd"] = "start-test"
wsChannel := session.OutputWS[sid]
err := wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Warn(err)
return
}
wsChannel.Refresh()
}
reader := bufio.NewReader(io.MultiReader(stdout, stderr))
if err := cmd.Start(); nil != err {
logger.Error(err)
result.Code = -1
return
}
go func(runningId int) {
defer gulu.Panic.Recover(nil)
logger.Debugf("User [%s, %s] is running [go test] [runningId=%d]", uid, sid, runningId)
channelRet := map[string]interface{}{}
channelRet["cmd"] = "go test"
// read all
buf, _ := ioutil.ReadAll(reader)
// waiting for go test finished
cmd.Wait()
if !cmd.ProcessState.Success() {
logger.Debugf("User [%s, %s] 's running [go test] [runningId=%d] has done (with error)", uid, sid, runningId)
channelRet["output"] = "<span class='test-error'>" + i18n.Get(locale, "test-error").(string) + "</span>\n" + string(buf)
} else {
logger.Debugf("User [%s, %s] 's running [go test] [runningId=%d] has done", uid, sid, runningId)
channelRet["output"] = "<span class='test-succ'>" + i18n.Get(locale, "test-succ").(string) + "</span>\n" + string(buf)
}
if nil != session.OutputWS[sid] {
wsChannel := session.OutputWS[sid]
err := wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Warn(err)
}
wsChannel.Refresh()
}
}(rand.Int())
}

View File

@ -1,149 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package output
import (
"bufio"
"encoding/json"
"io"
"io/ioutil"
"math/rand"
"net/http"
"os/exec"
"path/filepath"
"github.com/88250/gulu"
"github.com/88250/wide/conf"
"github.com/88250/wide/i18n"
"github.com/88250/wide/session"
)
// GoVetHandler handles request of go vet.
func GoVetHandler(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult()
defer gulu.Ret.RetResult(w, r, result)
httpSession, _ := session.HTTPSession.Get(r, session.CookieName)
if httpSession.IsNew {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
uid := httpSession.Values["uid"].(string)
locale := conf.GetUser(uid).Locale
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
result.Code = -1
return
}
sid := args["sid"].(string)
filePath := args["file"].(string)
curDir := filepath.Dir(filePath)
cmd := exec.Command("go", "vet", ".")
cmd.Dir = curDir
setCmdEnv(cmd, uid)
stdout, err := cmd.StdoutPipe()
if nil != err {
logger.Error(err)
result.Code = -1
return
}
stderr, err := cmd.StderrPipe()
if nil != err {
logger.Error(err)
result.Code = -1
return
}
if 0 != result.Code {
return
}
channelRet := map[string]interface{}{}
if nil != session.OutputWS[sid] {
// display "START [go vet]" in front-end browser
channelRet["output"] = "<span class='start-vet'>" + i18n.Get(locale, "start-vet").(string) + "</span>\n"
channelRet["cmd"] = "start-vet"
wsChannel := session.OutputWS[sid]
err := wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Warn(err)
return
}
wsChannel.Refresh()
}
reader := bufio.NewReader(io.MultiReader(stdout, stderr))
if err := cmd.Start(); nil != err {
logger.Error(err)
result.Code = -1
return
}
go func(runningId int) {
defer gulu.Panic.Recover(nil)
logger.Debugf("User [%s, %s] is running [go vet] [runningId=%d]", uid, sid, runningId)
channelRet := map[string]interface{}{}
channelRet["cmd"] = "go vet"
// read all
buf, _ := ioutil.ReadAll(reader)
// waiting for go vet finished
cmd.Wait()
if !cmd.ProcessState.Success() {
logger.Debugf("User [%s, %s] 's running [go vet] [runningId=%d] has done (with error)", uid, sid, runningId)
channelRet["output"] = "<span class='vet-error'>" + i18n.Get(locale, "vet-error").(string) + "</span>\n" + string(buf)
} else {
logger.Debugf("User [%s, %s] 's running [go vet] [runningId=%d] has done", uid, sid, runningId)
channelRet["output"] = "<span class='vet-succ'>" + i18n.Get(locale, "vet-succ").(string) + "</span>\n" + string(buf)
}
if nil != session.OutputWS[sid] {
wsChannel := session.OutputWS[sid]
err := wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Warn(err)
}
wsChannel.Refresh()
}
}(rand.Int())
}

3773
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +0,0 @@
{
"name": "wide",
"version": "1.6.0",
"description": "A Web-based Go IDE , do your development anytime, anywhere.",
"homepage": "https://wide.b3log.org",
"repository": {
"type": "git",
"url": "git://github.com/88250/wide.git"
},
"bugs": {
"url": "https://github.com/88250/wide/issues"
},
"license": "Apache License",
"private": true,
"author": "Daniel <d@b3log.org> (http://88250.b3log.org) & Vanessa <v@b3log.org> (http://vanessa.b3log.org)",
"maintainers": [
{
"name": "Daniel",
"email": "d@b3log.org"
},
{
"name": "Vanessa",
"email": "v@b3log.org"
}
],
"scripts": {
"build": "gulp"
},
"devDependencies": {
"gulp": "^4.0.2",
"gulp-clean-css": "^4.2.0",
"gulp-concat": "^2.6.1",
"gulp-sourcemaps": "^2.6.5",
"gulp-uglify": "^3.0.1"
}
}

80
pkg.sh
View File

@ -1,80 +0,0 @@
#!/bin/bash
# Wide package tool.
#
# Command:
# ./pkg.sh ${version} ${target}
# Example:
# ./pkg.sh 1.0.0 /home/daniel/1.0.0/
ver=$1
target=$2
list="conf doc i18n static views README.md TERMS.md LICENSE"
mkdir -p ${target}
echo version=${ver}
echo target=${target}
## darwin
os=darwin
export GOOS=${os}
export GOARCH=amd64
echo wide-${ver}-${GOOS}-${GOARCH}.tar.gz
go build
go build github.com/visualfc/gotools
go build github.com/stamblerre/gocode
tar zcf ${target}/wide-${ver}-${GOOS}-${GOARCH}.tar.gz ${list} gotools gocode wide --exclude-vcs --exclude='conf/*.go' --exclude='i18n/*.go'
rm -f wide gotools gocode
export GOOS=${os}
export GOARCH=386
echo wide-${ver}-${GOOS}-${GOARCH}.tar.gz
go build
go build github.com/visualfc/gotools
go build github.com/stamblerre/gocode
tar zcf ${target}/wide-${ver}-${GOOS}-${GOARCH}.tar.gz ${list} gotools gocode wide --exclude-vcs --exclude='conf/*.go' --exclude='i18n/*.go'
rm -f wide gotools gocode
## linux
os=linux
export GOOS=${os}
export GOARCH=amd64
echo wide-${ver}-${GOOS}-${GOARCH}.tar.gz
go build
go build github.com/visualfc/gotools
go build github.com/stamblerre/gocode
tar zcf ${target}/wide-${ver}-${GOOS}-${GOARCH}.tar.gz ${list} gotools gocode wide --exclude-vcs --exclude='conf/*.go' --exclude='i18n/*.go'
rm -f wide gotools gocode
export GOOS=${os}
export GOARCH=386
echo wide-${ver}-${GOOS}-${GOARCH}.tar.gz
go build
go build github.com/visualfc/gotools
go build github.com/stamblerre/gocode
tar zcf ${target}/wide-${ver}-${GOOS}-${GOARCH}.tar.gz ${list} gotools gocode wide --exclude-vcs --exclude='conf/*.go' --exclude='i18n/*.go'
rm -f wide gotools gocode
## windows
os=windows
export GOOS=${os}
export GOARCH=amd64
echo wide-${ver}-${GOOS}-${GOARCH}.zip
go build
go build github.com/visualfc/gotools
go build github.com/stamblerre/gocode
zip -r -q ${target}/wide-${ver}-${GOOS}-${GOARCH}.zip ${list} gotools.exe gocode.exe wide.exe --exclude=conf/*.go --exclude=i18n/*.go
rm -f wide.exe gotools.exe gocode.exe
export GOOS=${os}
export GOARCH=386
echo wide-${ver}-${GOOS}-${GOARCH}.zip
go build
go build github.com/visualfc/gotools
go build github.com/stamblerre/gocode
zip -r -q ${target}/wide-${ver}-${GOOS}-${GOARCH}.zip ${list} gotools.exe gocode.exe wide.exe --exclude=conf/*.go --exclude=i18n/*.go
rm -f wide.exe gotools.exe gocode.exe

View File

@ -1,111 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package playground
import (
"bytes"
"encoding/json"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"github.com/88250/gulu"
"github.com/88250/wide/conf"
"github.com/88250/wide/session"
)
// AutocompleteHandler handles request of code autocompletion.
func AutocompleteHandler(w http.ResponseWriter, r *http.Request) {
if conf.Wide.ReadOnly {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session, _ := session.HTTPSession.Get(r, session.CookieName)
if session.IsNew {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
code := args["code"].(string)
line := int(args["cursorLine"].(float64))
ch := int(args["cursorCh"].(float64))
file, err := os.Create("wide_autocomplete_" + gulu.Rand.String(16) + ".go")
if nil != err {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
file.WriteString(code)
file.Close()
path := file.Name()
defer os.Remove(path)
offset := getCursorOffset(code, line, ch)
argv := []string{"-f=json", "--in=" + path, "autocomplete", strconv.Itoa(offset)}
gocode := gulu.Go.GetExecutableInGOBIN("gocode")
cmd := exec.Command(gocode, argv...)
output, err := cmd.CombinedOutput()
if nil != err {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(output)
}
// getCursorOffset calculates the cursor offset.
//
// line is the line number, starts with 0 that means the first line
// ch is the column number, starts with 0 that means the first column
func getCursorOffset(code string, line, ch int) (offset int) {
lines := strings.Split(code, "\n")
// calculate sum length of lines before
for i := 0; i < line; i++ {
offset += len(lines[i])
}
// calculate length of the current line and column
curLine := lines[line]
var buffer bytes.Buffer
r := []rune(curLine)
for i := 0; i < ch; i++ {
buffer.WriteString(string(r[i]))
}
offset += len(buffer.String()) // append length of current line
offset += line // append number of '\n'
return offset
}

View File

@ -1,81 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package playground
import (
"encoding/json"
"html/template"
"net/http"
"os/exec"
"path/filepath"
"strings"
"github.com/88250/gulu"
"github.com/88250/wide/conf"
"github.com/88250/wide/session"
)
// BuildHandler handles request of Playground building.
func BuildHandler(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult()
defer gulu.Ret.RetResult(w, r, result)
if conf.Wide.ReadOnly {
result.Code = -1
result.Msg = "readonly mode"
return
}
httpSession, _ := session.HTTPSession.Get(r, session.CookieName)
if httpSession.IsNew {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
result.Code = -1
return
}
fileName := args["fileName"].(string)
filePath := filepath.Clean(conf.Wide.Data + "/playground/" + fileName)
suffix := ""
if gulu.OS.IsWindows() {
suffix = ".exe"
}
data := map[string]interface{}{}
result.Data = &data
executable := filepath.Clean(conf.Wide.Data + "/playground/" + strings.Replace(fileName, ".go", suffix, -1))
cmd := exec.Command("go", "build", "-o", executable, filePath)
out, err := cmd.CombinedOutput()
data["output"] = template.HTML(string(out))
if nil != err {
result.Code = -1
return
}
data["executable"] = executable
}

View File

@ -1,102 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package playground
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"github.com/88250/gulu"
"github.com/88250/wide/conf"
"github.com/88250/wide/session"
)
// SaveHandler handles request of Playground code save.
func SaveHandler(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult()
defer gulu.Ret.RetResult(w, r, result)
if conf.Wide.ReadOnly {
result.Code = -1
result.Msg = "readonly mode"
return
}
session, _ := session.HTTPSession.Get(r, session.CookieName)
if session.IsNew {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
result.Code = -1
return
}
code := args["code"].(string)
// Step1. format code
cmd := exec.Command("gofmt")
stdin, err := cmd.StdinPipe()
if nil != err {
logger.Error(err)
result.Code = -1
return
}
io.WriteString(stdin, code)
stdin.Close()
bytes, _ := cmd.Output()
output := string(bytes)
if "" != output {
code = string(output)
}
data := map[string]interface{}{}
result.Data = &data
data["code"] = code
// Step2. generate file name
hasher := md5.New()
hasher.Write([]byte(code))
fileName := hex.EncodeToString(hasher.Sum(nil))
fileName += ".go"
data["fileName"] = fileName
// Step3. write file
filePath := filepath.Clean(conf.Wide.Data + "/playground/" + fileName)
fout, err := os.Create(filePath)
fout.WriteString(code)
if err := fout.Close(); nil != err {
logger.Error(err)
result.Code = -1
return
}
}

View File

@ -1,123 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package shell include playground related mainipulations.
package playground
import (
"html/template"
"io/ioutil"
"math/rand"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/88250/gulu"
"github.com/88250/wide/conf"
"github.com/88250/wide/i18n"
"github.com/88250/wide/session"
"github.com/88250/wide/util"
"github.com/gorilla/websocket"
)
// Logger.
var logger = gulu.Log.NewLogger(os.Stdout)
// IndexHandler handles request of Playground index.
func IndexHandler(w http.ResponseWriter, r *http.Request) {
// create a HTTP session
httpSession, _ := session.HTTPSession.Get(r, session.CookieName)
if httpSession.IsNew {
httpSession.Values["id"] = strconv.Itoa(rand.Int())
httpSession.Values["uid"] = "playground"
}
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
httpSession.Save(r, w)
uid := httpSession.Values["uid"].(string)
locale := conf.Wide.Locale
// try to load file
code := conf.HelloWorld
fileName := "6c5595ec6fbadf4cfce3edbfcfd8c6d0.go" // MD5 of HelloWorld.go
if strings.HasSuffix(r.URL.Path, ".go") {
fileNameArg := r.URL.Path[len("/playground/"):]
filePath := filepath.Clean(conf.Wide.Data + "/playground/" + fileNameArg)
bytes, err := ioutil.ReadFile(filePath)
if nil != err {
logger.Warn(err)
} else {
code = string(bytes)
fileName = fileNameArg
}
}
query := r.URL.Query()
embed := false
embedArg, ok := query["embed"]
if ok && "true" == embedArg[0] {
embed = true
}
disqus := false
disqusArg, ok := query["disqus"]
if ok && "true" == disqusArg[0] {
disqus = true
}
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale,
"sid": session.WideSessions.GenId(), "pathSeparator": conf.PathSeparator,
"codeMirrorVer": conf.CodeMirrorVer,
"code": template.HTML(code), "ver": conf.WideVersion, "year": time.Now().Year(),
"embed": embed, "disqus": disqus, "fileName": fileName}
wideSessions := session.WideSessions.GetByUserId(uid)
logger.Debugf("User [%s] has [%d] sessions", uid, len(wideSessions))
t, err := template.ParseFiles("views/playground/index.html")
if nil != err {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
t.Execute(w, model)
}
// WSHandler handles request of creating Playground channel.
func WSHandler(w http.ResponseWriter, r *http.Request) {
sid := r.URL.Query()["sid"][0]
conn, _ := websocket.Upgrade(w, r, nil, 1024, 1024)
wsChan := util.WSChannel{Sid: sid, Conn: conn, Request: r, Time: time.Now()}
ret := map[string]interface{}{"output": "Playground initialized", "cmd": "init-playground"}
err := wsChan.WriteJSON(&ret)
if nil != err {
return
}
session.PlaygroundWS[sid] = &wsChan
logger.Tracef("Open a new [PlaygroundWS] with session [%s], %d", sid, len(session.PlaygroundWS))
}

View File

@ -1,30 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package playground
import (
"github.com/88250/wide/session"
"net/http"
)
// RunHandler handles request of executing a binary file.
func RunHandler(w http.ResponseWriter, r *http.Request) {
session.RunHandler(w, r, session.PlaygroundWS)
}
// StopHandler handles request of stopping a running process.
func StopHandler(w http.ResponseWriter, r *http.Request) {
session.StopHandler(w, r)
}

View File

@ -1,185 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package session
import (
"fmt"
"html/template"
"math/rand"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/88250/gulu"
"github.com/88250/wide/conf"
"github.com/88250/wide/i18n"
"github.com/88250/wide/util"
)
var states = map[string]string{}
// LoginRedirectHandler redirects to HacPai auth page.
func LoginRedirectHandler(w http.ResponseWriter, r *http.Request) {
loginAuthURL := conf.Wide.OAuthLoginURL + "?response_type=code&redirect_uri=" + conf.Wide.Server + "/login/callback"
// надо будет добавить ttlcache для state и проверять для предотвращения атак CSRF
state := gulu.Rand.String(16)
states[state] = state
path := loginAuthURL + "&state=" + state + "&client_id=" + conf.Wide.OAuthClientID
http.Redirect(w, r, path, http.StatusSeeOther)
}
func LoginCallbackHandler(w http.ResponseWriter, r *http.Request) {
state := r.URL.Query().Get("state")
if _, exist := states[state]; !exist {
http.Error(w, "Get state param failed", http.StatusBadRequest)
return
}
delete(states, state)
code := r.URL.Query().Get("code")
accessToken, err := util.GetOAuthToken(
conf.Wide.OAuthAccessTokenURL,
conf.Wide.OAuthClientID,
conf.Wide.OAuthClientSecret,
code,
conf.Wide.Server+`/login/callback`)
if err != nil {
http.Error(w, fmt.Sprintf(`get access_token failed. error: %s`, err.Error()), http.StatusBadRequest)
return
}
userInfo, err := util.OpenIdUserInfo(conf.Wide.OAuthUserInfoURL, accessToken)
if err != nil {
http.Error(w, fmt.Sprintf(`get user_info failed. error: %s`, err.Error()), http.StatusBadRequest)
return
}
userId := userInfo["userId"].(string)
userName := userInfo["userName"].(string)
avatar := userInfo["avatar"].(string)
user := conf.GetUser(userId)
if nil == user {
msg := addUser(userId, userName, avatar)
if userCreated != msg {
result := gulu.Ret.NewResult()
result.Code = -1
result.Msg = msg
gulu.Ret.RetResult(w, r, result)
return
}
}
// create a HTTP session
httpSession, _ := HTTPSession.Get(r, CookieName)
httpSession.Values["uid"] = userId
httpSession.Values["id"] = strconv.Itoa(rand.Int())
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
httpSession.Save(r, w)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
// LoginHandler handles request of show login page.
func LoginHandler(w http.ResponseWriter, r *http.Request) {
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(conf.Wide.Locale),
"locale": conf.Wide.Locale, "ver": conf.WideVersion, "year": time.Now().Year()}
t, err := template.ParseFiles("views/login.html")
if nil != err {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
t.Execute(w, model)
}
// LogoutHandler handles request of user logout (exit).
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult()
defer gulu.Ret.RetResult(w, r, result)
httpSession, _ := HTTPSession.Get(r, CookieName)
httpSession.Options.MaxAge = -1
httpSession.Save(r, w)
}
// addUser add a user with the specified user id, username and avatar.
//
// 1. create the user's workspace
// 2. generate 'Hello, 世界' demo code in the workspace (a console version and a HTTP version)
// 3. update the user customized configurations, such as style.css
// 4. serve files of the user's workspace via HTTP
//
// Note: user [playground] is a reserved mock user
func addUser(userId, userName, userAvatar string) string {
if "playground" == userId {
return userExists
}
addUserMutex.Lock()
defer addUserMutex.Unlock()
for _, user := range conf.Users {
if strings.ToLower(user.Id) == strings.ToLower(userId) {
return userExists
}
}
workspace := filepath.Join(conf.Wide.Data, "workspaces", userId)
newUser := conf.NewUser(userId, userName, userAvatar, workspace)
conf.Users = append(conf.Users, newUser)
if !newUser.Save() {
return userCreateError
}
conf.CreateWorkspaceDir(workspace)
helloWorld(workspace)
conf.UpdateCustomizedConf(userId)
logger.Infof("Created a user [%s]", userId)
return userCreated
}
// helloWorld generates the 'Hello, 世界' source code.
func helloWorld(workspace string) {
dir := workspace + conf.PathSeparator + "src" + conf.PathSeparator + "hello"
if err := os.MkdirAll(dir, 0755); nil != err {
logger.Error(err)
return
}
fout, err := os.Create(dir + conf.PathSeparator + "main.go")
if nil != err {
logger.Error(err)
return
}
fout.WriteString(conf.HelloWorld)
fout.Close()
}

View File

@ -1,313 +0,0 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package session
import (
"bytes"
"encoding/json"
"math/rand"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/88250/gulu"
"github.com/88250/wide/conf"
"github.com/88250/wide/util"
)
// Type of process set.
type procs map[string][]*os.Process
// Processse of all users.
//
// <sid, []*os.Process>
var Processes = procs{}
// Exclusive lock.
var procMutex sync.Mutex
// RunHandler handles request of executing a binary file.
func RunHandler(w http.ResponseWriter, r *http.Request, channel map[string]*util.WSChannel) {
result := gulu.Ret.NewResult()
defer gulu.Ret.RetResult(w, r, result)
if conf.Wide.ReadOnly {
result.Code = -1
result.Msg = "readonly mode"
return
}
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
result.Code = -1
}
sid := args["sid"].(string)
wSession := WideSessions.Get(sid)
if nil == wSession {
result.Code = -1
}
filePath := args["executable"].(string)
randInt := rand.Int()
rid := strconv.Itoa(randInt)
var cmd *exec.Cmd
if conf.Docker {
fileName := filepath.Base(filePath)
cmd = exec.Command("docker", "run", "--rm", "--name", rid, "-v", filePath+":/"+fileName,
"--memory", "64M", "--cpus", "0.1",
conf.DockerImageGo, "/"+fileName)
} else {
cmd = exec.Command(filePath)
curDir := filepath.Dir(filePath)
cmd.Dir = curDir
}
outBuf := &bytes.Buffer{}
errBuf := &bytes.Buffer{}
cmd.Stdout = outBuf
cmd.Stderr = errBuf
if err := cmd.Start(); nil != err {
logger.Error(err)
result.Code = -1
}
wsChannel := channel[sid]
channelRet := map[string]interface{}{}
if 0 != result.Code {
channelRet["cmd"] = "run-done"
channelRet["output"] = ""
wsChannel.WriteJSON(&channelRet)
wsChannel.Refresh()
return
}
done := make(chan error)
go func() { done <- cmd.Wait() }()
channelRet["pid"] = cmd.Process.Pid
Processes.Add(wSession, cmd.Process)
shouldExitBuf := false
// push once for front-end to get the 'run' state and pid
if nil != wsChannel {
channelRet["cmd"] = "run"
channelRet["output"] = ""
if nil != wsChannel {
wsChannel.WriteJSON(&channelRet)
wsChannel.Refresh()
}
}
go func() {
defer gulu.Panic.Recover(nil)
logger.Debugf("User [%s, %s] is running [id=%s, file=%s]", wSession.UserId, sid, rid, filePath)
go func() {
defer gulu.Panic.Recover(nil)
for {
if shouldExitBuf {
break
}
if 1 > outBuf.Len() {
time.Sleep(7 * time.Millisecond)
continue
}
r, _, err := outBuf.ReadRune()
if nil != err {
time.Sleep(7 * time.Millisecond)
continue
}
oneRuneStr := string(r)
oneRuneStr = strings.Replace(oneRuneStr, "<", "&lt;", -1)
oneRuneStr = strings.Replace(oneRuneStr, ">", "&gt;", -1)
channelRet["cmd"] = "run"
channelRet["output"] = oneRuneStr
wsChannel := channel[sid]
if nil != wsChannel {
wsChannel.WriteJSON(&channelRet)
wsChannel.Refresh()
}
}
}()
for {
if shouldExitBuf {
break
}
if 1 > errBuf.Len() {
time.Sleep(7 * time.Millisecond)
continue
}
r, _, err := errBuf.ReadRune()
if nil != err {
time.Sleep(7 * time.Millisecond)
continue
}
oneRuneStr := string(r)
oneRuneStr = strings.Replace(oneRuneStr, "<", "&lt;", -1)
oneRuneStr = strings.Replace(oneRuneStr, ">", "&gt;", -1)
channelRet["cmd"] = "run"
channelRet["output"] = "<span class='stderr'>" + oneRuneStr + "</span>"
wsChannel := channel[sid]
if nil != wsChannel {
wsChannel.WriteJSON(&channelRet)
wsChannel.Refresh()
}
}
}()
after := time.After(5 * time.Second)
kill := false
select {
case <-after:
if conf.Docker {
killCmd := exec.Command("docker", "rm", "-f", rid)
if err := killCmd.Run(); nil != err {
logger.Errorf("executes [docker rm -f " + rid + "] failed [" + err.Error() + "], this will cause resource leaking")
}
} else {
cmd.Process.Kill()
}
channelRet["output"] = "\n<span class='stderr'>run program timeout in 5s</span>\n"
kill = true
case <-done:
channelRet["output"] = "\n<span class='stderr'>run program complete</span>\n"
}
shouldExitBuf = true
Processes.Remove(wSession, cmd.Process)
logger.Debugf("User [%s, %s] done running [id=%s, file=%s, kill=%v]", wSession.UserId, sid, rid, filePath, kill)
if nil != wsChannel {
channelRet["cmd"] = "run-done"
wsChannel.WriteJSON(&channelRet)
wsChannel.Refresh()
}
}
// StopHandler handles request of stopping a running process.
func StopHandler(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult()
defer gulu.Ret.RetResult(w, r, result)
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
result.Code = -1
return
}
sid := args["sid"].(string)
pid := int(args["pid"].(float64))
wSession := WideSessions.Get(sid)
if nil == wSession {
result.Code = -1
return
}
Processes.Kill(wSession, pid)
}
// Add adds the specified process to the user process set.
func (procs *procs) Add(wSession *WideSession, proc *os.Process) {
procMutex.Lock()
defer procMutex.Unlock()
sid := wSession.ID
userProcesses := (*procs)[sid]
userProcesses = append(userProcesses, proc)
(*procs)[sid] = userProcesses
// bind process with wide session
wSession.SetProcesses(userProcesses)
logger.Tracef("Session [%s] has [%d] processes", sid, len((*procs)[sid]))
}
// Remove removes the specified process from the user process set.
func (procs *procs) Remove(wSession *WideSession, proc *os.Process) {
procMutex.Lock()
defer procMutex.Unlock()
sid := wSession.ID
userProcesses := (*procs)[sid]
var newProcesses []*os.Process
for i, p := range userProcesses {
if p.Pid == proc.Pid {
newProcesses = append(userProcesses[:i], userProcesses[i+1:]...) // remove it
(*procs)[sid] = newProcesses
// bind process with wide session
wSession.SetProcesses(newProcesses)
logger.Tracef("Session [%s] has [%d] processes", sid, len((*procs)[sid]))
return
}
}
}
// Kill kills a process specified by the given pid.
func (procs *procs) Kill(wSession *WideSession, pid int) {
procMutex.Lock()
defer procMutex.Unlock()
sid := wSession.ID
userProcesses := (*procs)[sid]
for i, p := range userProcesses {
if p.Pid == pid {
if err := p.Kill(); nil != err {
logger.Errorf("Kill a process [pid=%d] of user [%s, %s] failed [error=%v]", pid, wSession.UserId, sid, err)
} else {
var newProcesses []*os.Process
newProcesses = append(userProcesses[:i], userProcesses[i+1:]...)
(*procs)[sid] = newProcesses
// bind process with wide session
wSession.SetProcesses(newProcesses)
logger.Debugf("Killed a process [pid=%d] of user [%s, %s]", pid, wSession.UserId, sid)
}
return
}
}
}

View File

@ -1,280 +1,127 @@
// Copyright (c) 2014-present, b3log.org // 会话操作.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Wide 服务器端需要维护两种会话:
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// //
// https://www.apache.org/licenses/LICENSE-2.0 // 1. HTTP 会话:主要用于验证登录
// 2. Wide 会话:浏览器 tab 打开/刷新会创建一个,并和 HTTP 会话进行关联
// //
// Unless required by applicable law or agreed to in writing, software // 当会话失效时:释放所有和该会话相关的资源,例如运行中的程序进程、事件队列等.
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package session includes session related manipulations.
//
// Wide server side needs maintain two kinds of sessions:
//
// 1. HTTP session: mainly used for login authentication
// 2. Wide session: browser tab open/refresh will create one, and associates with HTTP session
//
// When a session gone: release all resources associated with it, such as running processes, event queues.
package session package session
import ( import (
"bytes"
"encoding/json" "encoding/json"
"math/rand" "math/rand"
"net/http" "net/http"
"os" "os"
"path/filepath"
"sort"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
"github.com/88250/gulu" "github.com/b3log/wide/conf"
"github.com/88250/wide/conf" "github.com/b3log/wide/event"
"github.com/88250/wide/event" "github.com/b3log/wide/util"
"github.com/88250/wide/util" "github.com/golang/glog"
"github.com/fsnotify/fsnotify"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
const ( const (
sessionStateActive = iota SessionStateActive = iota // 会话状态:活的
sessionStateClosed // (not used so far) SessionStateClosed // 会话状态:已关闭(这个状态目前暂时没有使用到)
CookieName = "wide-sess"
) )
// Logger.
var logger = gulu.Log.NewLogger(os.Stdout)
var ( var (
// SessionWS holds all session channels. <sid, *util.WSChannel> // 会话通道. <sid, *util.WSChannel>
SessionWS = map[string]*util.WSChannel{} SessionWS = map[string]*util.WSChannel{}
// EditorWS holds all editor channels. <sid, *util.WSChannel> // 输出通道. <sid, *util.WSChannel>
EditorWS = map[string]*util.WSChannel{}
// OutputWS holds all output channels. <sid, *util.WSChannel>
OutputWS = map[string]*util.WSChannel{} OutputWS = map[string]*util.WSChannel{}
// NotificationWS holds all notification channels. <sid, *util.WSChannel> // 通知通道. <sid, *util.WSChannel>
NotificationWS = map[string]*util.WSChannel{} NotificationWS = map[string]*util.WSChannel{}
// PlaygroundWS holds all playground channels. <sid, *util.WSChannel>
PlaygroundWS = map[string]*util.WSChannel{}
) )
// HTTP session store. // 用户 HTTP 会话,用于验证登录.
var HTTPSession = sessions.NewCookieStore([]byte("BEYOND")) var HTTPSession = sessions.NewCookieStore([]byte("BEYOND"))
// WideSession represents a session associated with a browser tab. // Wide 会话,对应一个浏览器 tab.
type WideSession struct { type WideSession struct {
ID string // id Id string // 唯一标识
UserId string // user id Username string // 用户名
HTTPSession *sessions.Session // HTTP session related HTTPSession *sessions.Session // 关联的 HTTP 会话
Processes []*os.Process // process set Processes []*os.Process // 关联的进程集
EventQueue *event.UserEventQueue // event queue EventQueue *event.UserEventQueue // 关联的事件队列
State int // state State int // 状态
Content *conf.LatestSessionContent // the latest session content Content *conf.LatestSessionContent // 最近一次会话内容
FileWatcher *fsnotify.Watcher // files change watcher Created time.Time // 创建时间
Created time.Time // create time Updated time.Time // 最近一次使用时间
Updated time.Time // the latest use time
} }
// Type of wide sessions. // 会话集类型.
type wSessions []*WideSession type Sessions []*WideSession
// Wide sessions. // 所有 Wide 会话集.
var WideSessions wSessions var WideSessions Sessions
// Exclusive lock. // 排它锁,防止并发修改.
var mutex sync.Mutex var mutex sync.Mutex
// FixedTimeRelease releases invalid sessions. // 在一些特殊情况(例如浏览器不间断刷新/在源代码视图刷新)下 Wide 会话集内会出现无效会话该函数定时1 小时)检查并移除这些无效会话.
// //
// In some special cases (such as a browser uninterrupted refresh / refresh in the source code view) will occur // 无效会话:在检查时间内 30 分钟都没有使用过的会话,参考 WideSession.Updated 字段.
// some invalid sessions, the function checks and removes these invalid sessions periodically (1 hour).
//
// Invalid sessions: sessions that not used within 30 minutes, refers to WideSession.Updated field.
func FixedTimeRelease() { func FixedTimeRelease() {
go func() { go func() {
defer gulu.Panic.Recover(nil) // 1 小时进行一次检查
for _ = range time.Tick(time.Hour) { for _ = range time.Tick(time.Hour) {
hour, _ := time.ParseDuration("-30m") hour, _ := time.ParseDuration("-30m")
threshold := time.Now().Add(hour) threshold := time.Now().Add(hour)
for _, s := range WideSessions { for _, s := range WideSessions {
if s.Updated.Before(threshold) { if s.Updated.Before(threshold) {
logger.Debugf("Removes a invalid session [%s], user [%s]", s.ID, s.UserId) glog.V(3).Infof("Removes a invalid session [%s]", s.Id)
WideSessions.Remove(s.ID) WideSessions.Remove(s.Id)
} }
} }
} }
}() }()
} }
// Online user statistic report. // 建立会话通道. 通道断开时销毁会话状态,回收相关资源.
type userReport struct {
userId string
sessionCnt int
processCnt int
updated time.Time
}
// report returns a online user statistics in pretty format.
func (u *userReport) report() string {
return "[" + u.userId + "] has [" + strconv.Itoa(u.sessionCnt) + "] sessions and [" + strconv.Itoa(u.processCnt) +
"] running processes, latest activity [" + u.updated.Format("2006-01-02 15:04:05") + "]"
}
// FixedTimeReport reports the Wide sessions status periodically (10 minutes).
func FixedTimeReport() {
go func() {
defer gulu.Panic.Recover(nil)
for _ = range time.Tick(10 * time.Minute) {
users := userReports{}
processSum := 0
for _, s := range WideSessions {
processCnt := len(s.Processes)
processSum += processCnt
if report, exists := contains(users, s.UserId); exists {
if s.Updated.After(report.updated) {
report.updated = s.Updated
}
report.sessionCnt++
report.processCnt += processCnt
} else {
users = append(users, &userReport{userId: s.UserId, sessionCnt: 1, processCnt: processCnt, updated: s.Updated})
}
}
var buf bytes.Buffer
buf.WriteString("\n [" + strconv.Itoa(len(users)) + "] users, [" + strconv.Itoa(processSum) + "] running processes and [" +
strconv.Itoa(len(WideSessions)) + "] sessions currently\n")
sort.Sort(users)
for _, t := range users {
buf.WriteString(" " + t.report() + "\n")
}
logger.Info(buf.String())
}
}()
}
func contains(reports []*userReport, userId string) (*userReport, bool) {
for _, ur := range reports {
if userId == ur.userId {
return ur, true
}
}
return nil, false
}
type userReports []*userReport
func (f userReports) Len() int { return len(f) }
func (f userReports) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
func (f userReports) Less(i, j int) bool { return f[i].processCnt > f[j].processCnt }
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
)
// WSHandler handles request of creating session channel.
//
// When a channel closed, releases all resources associated with it.
func WSHandler(w http.ResponseWriter, r *http.Request) { func WSHandler(w http.ResponseWriter, r *http.Request) {
sid := r.URL.Query()["sid"][0] sid := r.URL.Query()["sid"][0]
wSession := WideSessions.Get(sid)
if nil == wSession {
glog.Errorf("Session [%s] not found", sid)
return
}
conn, _ := websocket.Upgrade(w, r, nil, 1024, 1024) conn, _ := websocket.Upgrade(w, r, nil, 1024, 1024)
wsChan := util.WSChannel{Sid: sid, Conn: conn, Request: r, Time: time.Now()} wsChan := util.WSChannel{Sid: sid, Conn: conn, Request: r, Time: time.Now()}
ret := map[string]interface{}{"output": "Session initialized", "cmd": "init-session"}
err := wsChan.WriteJSON(&ret)
if nil != err {
return
}
SessionWS[sid] = &wsChan SessionWS[sid] = &wsChan
wSession := WideSessions.Get(sid) ret := map[string]interface{}{"output": "Ouput initialized", "cmd": "init-session"}
if nil == wSession { wsChan.Conn.WriteJSON(&ret)
httpSession, _ := HTTPSession.Get(r, CookieName)
if httpSession.IsNew { glog.V(4).Infof("Open a new [Session Channel] with session [%s], %d", sid, len(SessionWS))
return
}
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
httpSession.Save(r, w)
wSession = WideSessions.new(httpSession, sid)
logger.Tracef("Created a wide session [%s] for websocket reconnecting, user [%s]", sid, wSession.UserId)
}
logger.Tracef("Open a new [Session Channel] with session [%s], %d", sid, len(SessionWS))
input := map[string]interface{}{} input := map[string]interface{}{}
wsChan.Conn.SetReadDeadline(time.Now().Add(pongWait))
wsChan.Conn.SetPongHandler(func(string) error { wsChan.Conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
ticker := time.NewTicker(pingPeriod)
defer func() {
WideSessions.Remove(sid)
ticker.Stop()
wsChan.Close()
}()
// send websocket ping message.
go func(t *time.Ticker, channel util.WSChannel) {
for {
select {
case <-t.C:
if err := channel.Conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
return
}
}
}
}(ticker, wsChan)
for { for {
if err := wsChan.ReadJSON(&input); err != nil { if err := wsChan.Conn.ReadJSON(&input); err != nil {
logger.Tracef("[Session Channel] of session [%s] disconnected, releases all resources with it, user [%s]", sid, wSession.UserId) glog.V(3).Infof("[Session Channel] of session [%s] disconnected, releases all resources with it", sid)
WideSessions.Remove(sid)
return return
} }
ret = map[string]interface{}{"output": "", "cmd": "session-output"} ret = map[string]interface{}{"output": "", "cmd": "session-output"}
if err := wsChan.WriteJSON(&ret); err != nil { if err := wsChan.Conn.WriteJSON(&ret); err != nil {
logger.Error("Session WS ERROR: " + err.Error()) glog.Error("Session WS ERROR: " + err.Error())
return return
} }
@ -282,10 +129,10 @@ func WSHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
// SaveContentHandler handles request of session content string. // 会话内容保存.
func SaveContentHandler(w http.ResponseWriter, r *http.Request) { func SaveContent(w http.ResponseWriter, r *http.Request) {
result := gulu.Ret.NewResult() data := map[string]interface{}{"succ": true}
defer gulu.Ret.RetResult(w, r, result) defer util.RetJSON(w, r, data)
args := struct { args := struct {
Sid string Sid string
@ -293,28 +140,26 @@ func SaveContentHandler(w http.ResponseWriter, r *http.Request) {
}{} }{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil { if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err) glog.Error(err)
result.Code = -1 data["succ"] = false
return return
} }
wSession := WideSessions.Get(args.Sid) wSession := WideSessions.Get(args.Sid)
if nil == wSession { if nil == wSession {
result.Code = -1 data["succ"] = false
return return
} }
wSession.Content = args.LatestSessionContent wSession.Content = args.LatestSessionContent
for _, user := range conf.Users { for _, user := range conf.Wide.Users {
if user.Id == wSession.UserId { if user.Name == wSession.Username {
// update the variable in-memory, session.FixedTimeSave() function will persist it periodically // 更新配置内存变量conf.FixedTimeSave() 会负责定时持久化
user.LatestSessionContent = wSession.Content user.LatestSessionContent = wSession.Content
user.Lived = time.Now().UnixNano()
wSession.Refresh() wSession.Refresh()
return return
@ -322,32 +167,54 @@ func SaveContentHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
// SetProcesses binds process set with the wide session. // 设置会话关联的进程集.
func (s *WideSession) SetProcesses(ps []*os.Process) { func (s *WideSession) SetProcesses(ps []*os.Process) {
s.Processes = ps s.Processes = ps
s.Refresh() s.Refresh()
} }
// Refresh refreshes the channel by updating its use time. // 刷新会话最近一次使用时间.
func (s *WideSession) Refresh() { func (s *WideSession) Refresh() {
s.Updated = time.Now() s.Updated = time.Now()
} }
// GenId generates a wide session id. // 创建一个 Wide 会话.
func (sessions *wSessions) GenId() string { func (sessions *Sessions) New(httpSession *sessions.Session) *WideSession {
mutex.Lock()
defer mutex.Unlock()
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
return strconv.Itoa(rand.Int()) id := strconv.Itoa(rand.Int())
now := time.Now()
// 创建用户事件队列
userEventQueue := event.UserEventQueues.New(id)
ret := &WideSession{
Id: id,
Username: httpSession.Values["username"].(string),
HTTPSession: httpSession,
EventQueue: userEventQueue,
State: SessionStateActive,
Content: &conf.LatestSessionContent{},
Created: now,
Updated: now,
}
*sessions = append(*sessions, ret)
return ret
} }
// Get gets a wide session with the specified session id. // 获取 Wide 会话.
func (sessions *wSessions) Get(sid string) *WideSession { func (sessions *Sessions) Get(sid string) *WideSession {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
for _, s := range *sessions { for _, s := range *sessions {
if s.ID == sid { if s.Id == sid {
return s return s
} }
} }
@ -355,36 +222,35 @@ func (sessions *wSessions) Get(sid string) *WideSession {
return nil return nil
} }
// Remove removes a wide session specified with the given session id, releases resources associated with it. // 移除 Wide 会话,释放相关资源.
// //
// Session-related resources: // 会话相关资源:
// //
// 1. user event queue // 1. 用户事件队列
// 2. process set // 2. 运行中的进程集
// 3. websocket channels // 3. WebSocket 通道
// 4. file watcher func (sessions *Sessions) Remove(sid string) {
func (sessions *wSessions) Remove(sid string) {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
for i, s := range *sessions { for i, s := range *sessions {
if s.ID == sid { if s.Id == sid {
// remove from session set // 从会话集中移除
*sessions = append((*sessions)[:i], (*sessions)[i+1:]...) *sessions = append((*sessions)[:i], (*sessions)[i+1:]...)
// close user event queue // 关闭用户事件队列
event.UserEventQueues.Close(sid) event.UserEventQueues.Close(sid)
// kill processes // 杀进程
for _, p := range s.Processes { for _, p := range s.Processes {
if err := p.Kill(); nil != err { if err := p.Kill(); nil != err {
logger.Errorf("Can't kill process [%d] of session [%s], user [%s]", p.Pid, sid, s.UserId) glog.Errorf("Can't kill process [%d] of session [%s]", p.Pid, sid)
} else { } else {
logger.Debugf("Killed a process [%d] of session [%s], user [%s]", p.Pid, sid, s.UserId) glog.V(3).Infof("Killed a process [%d] of session [%s]", p.Pid, sid)
} }
} }
// close websocket channels // 回收所有通道
if ws, ok := OutputWS[sid]; ok { if ws, ok := OutputWS[sid]; ok {
ws.Close() ws.Close()
delete(OutputWS, sid) delete(OutputWS, sid)
@ -400,155 +266,33 @@ func (sessions *wSessions) Remove(sid string) {
delete(SessionWS, sid) delete(SessionWS, sid)
} }
if ws, ok := PlaygroundWS[sid]; ok { glog.V(3).Infof("Removed a session [%s]", s.Id)
ws.Close()
delete(PlaygroundWS, sid)
}
// file watcher cnt := 0 // 统计当前 HTTP 会话关联的 Wide 会话数量
if nil != s.FileWatcher { for _, s := range *sessions {
s.FileWatcher.Close() if s.HTTPSession.ID == s.HTTPSession.ID {
}
cnt := 0 // count wide sessions associated with HTTP session
for _, ses := range *sessions {
if ses.UserId == s.UserId {
cnt++ cnt++
} }
} }
glog.V(3).Infof("User [%s] has [%d] sessions", s.Username, cnt)
logger.Debugf("Removed a session [%s] of user [%s], it has [%d] sessions currently", sid, s.UserId, cnt)
return return
} }
} }
} }
// GetByUsername gets wide sessions. // 获取 username 指定的用户的所有 Wide 会话.
func (sessions *wSessions) GetByUserId(userId string) []*WideSession { func (sessions *Sessions) GetByUsername(username string) []*WideSession {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
ret := []*WideSession{} ret := []*WideSession{}
for _, s := range *sessions { for _, s := range *sessions {
if s.UserId == userId { if s.Username == username {
ret = append(ret, s) ret = append(ret, s)
} }
} }
return ret return ret
} }
// new creates a wide session.
func (sessions *wSessions) new(httpSession *sessions.Session, sid string) *WideSession {
mutex.Lock()
defer mutex.Unlock()
uid := httpSession.Values["uid"].(string)
now := time.Now()
ret := &WideSession{
ID: sid,
UserId: uid,
HTTPSession: httpSession,
EventQueue: nil,
State: sessionStateActive,
Content: &conf.LatestSessionContent{},
Created: now,
Updated: now,
}
*sessions = append(*sessions, ret)
if "playground" == uid {
return ret
}
// create user event queue
ret.EventQueue = event.UserEventQueues.New(sid)
// add a filesystem watcher to notify front-end after the files changed
watcher, err := fsnotify.NewWatcher()
if err != nil {
logger.Error(err)
return ret
}
go func() {
defer gulu.Panic.Recover(nil)
for {
ch := SessionWS[sid]
if nil == ch {
return // release this goroutine
}
select {
case event := <-watcher.Events:
path := filepath.ToSlash(event.Name)
dir := filepath.ToSlash(filepath.Dir(path))
ch = SessionWS[sid]
if nil == ch {
return // release this goroutine
}
logger.Trace(event)
if event.Op&fsnotify.Create == fsnotify.Create {
fileType := "f"
if gulu.File.IsDir(path) {
fileType = "d"
if err = watcher.Add(path); nil != err {
logger.Warn(err, path)
}
}
cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "create-file", "type": fileType}
ch.WriteJSON(&cmd)
} else if event.Op&fsnotify.Remove == fsnotify.Remove {
cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "remove-file", "type": ""}
ch.WriteJSON(&cmd)
} else if event.Op&fsnotify.Rename == fsnotify.Rename {
cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "rename-file", "type": ""}
ch.WriteJSON(&cmd)
}
case err := <-watcher.Errors:
if nil != err {
logger.Error("File watcher ERROR: ", err)
}
}
}
}()
go func() {
defer gulu.Panic.Recover(nil)
workspaces := filepath.SplitList(conf.GetUserWorkspace(uid))
for _, workspace := range workspaces {
filepath.Walk(filepath.Join(workspace, "src"), func(dirPath string, f os.FileInfo, err error) error {
if strings.HasPrefix(f.Name(), ".") || "node_modules" == f.Name() || "vendor" == f.Name() {
return filepath.SkipDir
}
if f.IsDir() {
if err = watcher.Add(dirPath); nil != err {
logger.Error(err, dirPath)
}
logger.Tracef("File watcher added a dir [%s]", dirPath)
}
return nil
})
}
ret.FileWatcher = watcher
}()
return ret
}

View File

@ -1,220 +1,83 @@
// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package session package session
// TODO: 这个文件内的功能目前没有使用,只是开了个头 :p
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"path/filepath"
"runtime"
"strings"
"sync"
"text/template"
"time"
"github.com/88250/gulu" "github.com/b3log/wide/conf"
"github.com/88250/wide/conf" "github.com/b3log/wide/util"
"github.com/88250/wide/i18n" "github.com/golang/glog"
) )
const ( const (
// TODO: i18n USER_EXISTS = "user exists"
USER_CREATED = "user created"
userExists = "user exists" USER_CREATE_FAILED = "user create failed"
userCreated = "user created"
userCreateError = "user create error"
) )
// Exclusive lock for adding user. // 添加用户.
var addUserMutex sync.Mutex func AddUser(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{"succ": true}
defer util.RetJSON(w, r, data)
// PreferenceHandler handles request of preference page. decoder := json.NewDecoder(r.Body)
func PreferenceHandler(w http.ResponseWriter, r *http.Request) {
httpSession, _ := HTTPSession.Get(r, CookieName)
if httpSession.IsNew { var args map[string]interface{}
http.Redirect(w, r, "/login", http.StatusFound)
if err := decoder.Decode(&args); err != nil {
glog.Error(err)
data["succ"] = false
return return
} }
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge username := args["username"].(string)
httpSession.Save(r, w) password := args["password"].(string)
uid := httpSession.Values["uid"].(string) msg := addUser(username, password)
user := conf.GetUser(uid) if USER_CREATED != msg {
data["succ"] = false
if "GET" == r.Method { data["msg"] = msg
tmpLinux := user.GoBuildArgsForLinux
tmpWindows := user.GoBuildArgsForWindows
tmpDarwin := user.GoBuildArgsForDarwin
user.GoBuildArgsForLinux = strings.Replace(user.GoBuildArgsForLinux, `"`, `&quot;`, -1)
user.GoBuildArgsForWindows = strings.Replace(user.GoBuildArgsForWindows, `"`, `&quot;`, -1)
user.GoBuildArgsForDarwin = strings.Replace(user.GoBuildArgsForDarwin, `"`, `&quot;`, -1)
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(user.Locale), "user": user,
"ver": conf.WideVersion, "goos": runtime.GOOS, "goarch": runtime.GOARCH, "gover": runtime.Version(),
"locales": i18n.GetLocalesNames(), "gofmts": gulu.Go.GetGoFormats(),
"themes": conf.GetThemes(), "editorThemes": conf.GetEditorThemes()}
t, err := template.ParseFiles("views/preference.html")
if nil != err {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
user.GoBuildArgsForLinux = tmpLinux
user.GoBuildArgsForWindows = tmpWindows
user.GoBuildArgsForDarwin = tmpDarwin
return
}
t.Execute(w, model)
user.GoBuildArgsForLinux = tmpLinux
user.GoBuildArgsForWindows = tmpWindows
user.GoBuildArgsForDarwin = tmpDarwin
return
}
// non-GET request as save request
result := gulu.Ret.NewResult()
defer gulu.Ret.RetResult(w, r, result)
args := struct {
FontFamily string
FontSize string
GoFmt string
GoBuildArgsForLinux string
GoBuildArgsForWindows string
GoBuildArgsForDarwin string
Keymap string
Workspace string
Username string
Locale string
Theme string
EditorFontFamily string
EditorFontSize string
EditorLineHeight string
EditorTheme string
EditorTabSize string
}{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
result.Code = -1
return
}
user.FontFamily = args.FontFamily
user.FontSize = args.FontSize
user.GoFormat = args.GoFmt
user.GoBuildArgsForLinux = args.GoBuildArgsForLinux
user.GoBuildArgsForWindows = args.GoBuildArgsForWindows
user.GoBuildArgsForDarwin = args.GoBuildArgsForDarwin
user.Keymap = args.Keymap
// XXX: disallow change workspace at present
// user.Workspace = args.Workspace
user.Locale = args.Locale
user.Theme = args.Theme
user.Editor.FontFamily = args.EditorFontFamily
user.Editor.FontSize = args.EditorFontSize
user.Editor.LineHeight = args.EditorLineHeight
user.Editor.Theme = args.EditorTheme
user.Editor.TabSize = args.EditorTabSize
conf.UpdateCustomizedConf(uid)
now := time.Now().UnixNano()
user.Lived = now
user.Updated = now
if user.Save() {
result.Code = 0
} else {
result.Code = -1
} }
} }
// FixedTimeSave saves online users' configurations periodically (1 minute). // 初始化用户 git 仓库.
// func InitGitRepos(w http.ResponseWriter, r *http.Request) {
// Main goal of this function is to save user session content, for restoring session content while user open Wide next time. data := map[string]interface{}{"succ": true}
func FixedTimeSave() { defer util.RetJSON(w, r, data)
go func() {
defer gulu.Panic.Recover(nil)
for _ = range time.Tick(time.Minute) { session, _ := HTTPSession.Get(r, "wide-session")
SaveOnlineUsers()
} username := session.Values["username"].(string)
}() userRepos := conf.Wide.GetUserWorkspace(username) + conf.PathSeparator + "src"
glog.Info(userRepos)
// TODO: git clone
} }
// CanAccess determines whether the user specified by the given user id can access the specified path. func addUser(username, password string) string {
func CanAccess(userId, path string) bool { // TODO: https://github.com/b3log/wide/issues/23
path = filepath.FromSlash(path) conf.Load()
userWorkspace := conf.GetUserWorkspace(userId) // XXX: 新建用户校验增强
workspaces := filepath.SplitList(userWorkspace) for _, user := range conf.Wide.Users {
if user.Name == username {
for _, workspace := range workspaces { return USER_EXISTS
if strings.HasPrefix(path, workspace) {
return true
} }
} }
return false // FIXME: 新建用户时保存工作空间
} newUser := &conf.User{Name: username, Password: password, Workspace: ""}
conf.Wide.Users = append(conf.Wide.Users, newUser)
// SaveOnlineUsers saves online users' configurations at once. if !conf.Save() {
func SaveOnlineUsers() { return USER_CREATE_FAILED
users := getOnlineUsers()
for _, u := range users {
if u.Save() {
logger.Tracef("Saved online user [%s]'s configurations", u.Name)
}
}
}
func getOnlineUsers() []*conf.User {
ret := []*conf.User{}
uids := map[string]string{} // distinct uid
for _, s := range WideSessions {
uids[s.UserId] = s.UserId
} }
for _, uid := range uids { glog.Infof("Created a user [%s]", username)
u := conf.GetUser(uid)
if "playground" == uid { // user [playground] is a reserved mock user return USER_CREATED
continue
}
if nil == u {
logger.Warnf("Not found user [%s]", uid)
continue
}
ret = append(ret, u)
}
return ret
} }

176
shell/shells.go Normal file
View File

@ -0,0 +1,176 @@
// Shell.
package shell
import (
"html/template"
"net/http"
"os"
"os/exec"
"runtime"
"strings"
"time"
"github.com/b3log/wide/conf"
"github.com/b3log/wide/i18n"
"github.com/b3log/wide/session"
"github.com/b3log/wide/util"
"github.com/golang/glog"
"github.com/gorilla/websocket"
)
// Shell 通道.
//
// <sid, *util.WSChannel>>
var ShellWS = map[string]*util.WSChannel{}
// Shell 首页.
func IndexHandler(w http.ResponseWriter, r *http.Request) {
i18n.Load()
httpSession, _ := session.HTTPSession.Get(r, "wide-session")
if httpSession.IsNew {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
httpSession.Save(r, w)
// 创建一个 Wide 会话
wideSession := session.WideSessions.New(httpSession)
username := httpSession.Values["username"].(string)
locale := conf.Wide.GetUser(username).Locale
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale,
"session": wideSession}
wideSessions := session.WideSessions.GetByUsername(username)
glog.V(3).Infof("User [%s] has [%d] sessions", username, len(wideSessions))
t, err := template.ParseFiles("views/shell.html")
if nil != err {
glog.Error(err)
http.Error(w, err.Error(), 500)
return
}
t.Execute(w, model)
}
// 建立 Shell 通道.
func WSHandler(w http.ResponseWriter, r *http.Request) {
httpSession, _ := session.HTTPSession.Get(r, "wide-session")
username := httpSession.Values["username"].(string)
sid := r.URL.Query()["sid"][0]
conn, _ := websocket.Upgrade(w, r, nil, 1024, 1024)
wsChan := util.WSChannel{Sid: sid, Conn: conn, Request: r, Time: time.Now()}
ShellWS[sid] = &wsChan
ret := map[string]interface{}{"output": "Shell initialized", "cmd": "init-shell"}
wsChan.Conn.WriteJSON(&ret)
glog.V(4).Infof("Open a new [Shell] with session [%s], %d", sid, len(ShellWS))
input := map[string]interface{}{}
for {
if err := wsChan.Conn.ReadJSON(&input); err != nil {
if err.Error() == "EOF" {
return
}
if err.Error() == "unexpected EOF" {
return
}
glog.Error("Shell WS ERROR: " + err.Error())
return
}
inputCmd := input["cmd"].(string)
cmds := strings.Split(inputCmd, "|")
commands := []*exec.Cmd{}
for _, cmdWithArgs := range cmds {
cmdWithArgs = strings.TrimSpace(cmdWithArgs)
cmdWithArgs := strings.Split(cmdWithArgs, " ")
args := []string{}
if len(cmdWithArgs) > 1 {
args = cmdWithArgs[1:]
}
cmd := exec.Command(cmdWithArgs[0], args...)
commands = append(commands, cmd)
}
output := ""
if !strings.Contains(inputCmd, "clear") {
output = pipeCommands(username, commands...)
}
ret = map[string]interface{}{"output": output, "cmd": "shell-output"}
if err := wsChan.Conn.WriteJSON(&ret); err != nil {
glog.Error("Shell WS ERROR: " + err.Error())
return
}
// 更新通道最近使用时间
wsChan.Time = time.Now()
}
}
// 以管道方式执行多个命令.
func pipeCommands(username string, commands ...*exec.Cmd) string {
for i, command := range commands[:len(commands)-1] {
setCmdEnv(command, username)
stdout, err := command.StdoutPipe()
if nil != err {
return err.Error()
}
command.Start()
commands[i+1].Stdin = stdout
}
last := commands[len(commands)-1]
setCmdEnv(last, username)
out, err := last.CombinedOutput()
// 结束进程,释放资源
for _, command := range commands[:len(commands)-1] {
command.Wait()
}
if err != nil {
return err.Error()
}
return string(out)
}
func setCmdEnv(cmd *exec.Cmd, username string) {
userWorkspace := conf.Wide.GetUserWorkspace(username)
cmd.Env = append(cmd.Env,
"TERM="+os.Getenv("TERM"),
"GOPATH="+userWorkspace,
"GOOS="+runtime.GOOS,
"GOARCH="+runtime.GOARCH,
"GOROOT="+runtime.GOROOT(),
"PATH="+os.Getenv("PATH"))
cmd.Dir = userWorkspace
}

View File

@ -1,32 +1,12 @@
/*
* Copyright (c) 2014-present, b3log.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#dialogAboutDialog .dialog-main { #dialogAboutDialog .dialog-main {
background-color: #FFF; background-color: #FFF;
} }
#dialogAbout { #dialogAbout {
margin: 35px 20px; margin: 35px 100px 0 100px;
line-height: 28px; line-height: 28px;
} }
#dialogAbout .item {
margin: 0 10px;
}
#dialogAbout a { #dialogAbout a {
color: #4183c4; color: #4183c4;
text-decoration: none; text-decoration: none;
@ -48,17 +28,23 @@
#dialogAbout .space { #dialogAbout .space {
margin-bottom: 6px; margin-bottom: 6px;
border-bottom: 1px solid #919191; border-bottom: 1px solid #f1f1f1;
padding-bottom: 6px; padding-bottom: 6px;
} }
#dialogAbout .thx {
border-left: 1px solid #f1f1f1;
padding-left: 24px;
}
#dialogAbout .thx,
#dialogAbout .thx ul { #dialogAbout .thx ul {
margin-left: 50px; margin-left: 50px;
} }
#dialogAbout .thx a { #dialogAbout .thx a {
width: 80px; width: 100px;
display: inline-block; display: inline-block;
} }
@ -67,6 +53,5 @@
font-size: 12px; font-size: 12px;
line-height: normal; line-height: normal;
height: 85px; height: 85px;
overflow-x: hidden; overflow: auto;
word-wrap: break-word;
} }

View File

@ -1,56 +1,10 @@
/*
* Copyright (c) 2014-present, b3log.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* themes for base.
*
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
* @version 0.2.0.0, Oct 5, 2018
*/
/* start reset & function */ /* start reset & function */
::-webkit-scrollbar {
background: none;
width: 16px;
height: 16px;
}
::-webkit-scrollbar-corner {
display: none;
background-color: transparent;
}
::-webkit-scrollbar-thumb {
border: solid 0 rgba(0, 0, 0, 0);
border-right-width: 4px;
border-left-width: 4px;
border-radius: 9px;
box-shadow: inset 0 0 0 1px rgba(128, 128, 128, 0.2), inset 0 0 0 4px rgba(128, 128, 128, 0.2);
}
::-webkit-scrollbar-thumb:horizontal {
border-bottom-width: 4px;
border-top-width: 4px;
}
body { body {
font-size: 13px; font-size: 13px;
margin: 0; margin: 0;
color: #000; color: #000;
overflow: hidden; overflow: hidden;
font-family: Helvetica; font-family: Helvetica, 'Microsoft Yahei';
} }
ul { ul {
@ -63,22 +17,9 @@ ul {
box-sizing: border-box; box-sizing: border-box;
} }
a {
color: #4183c4;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
img {
vertical-align: middle;
}
input, input,
button { button {
font-family: Helvetica; font-family: Helvetica, 'Microsoft Yahei';
} }
.fn-left { .fn-left {
@ -103,203 +44,3 @@ button {
display: none; display: none;
} }
/* end reset & function */ /* end reset & function */
/* start common */
.ft-small {
color: #999;
font-size: 12px;
}
.ft-red {
color: #9d0000;
}
.list li {
cursor: pointer;
line-height: 20px;
padding: 0 3px;
word-wrap: normal;
word-break: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.list li.selected,
.list li:hover {
background-color: #3875d7;
color: #FFF;
}
.list li.selected .ft-small,
.list li:hover .ft-small {
color: #FFF;
}
/* end common */
/* start icon */
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?lqk80d');
src: url('fonts/icomoon.eot?lqk80d#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?lqk80d') format('truetype'),
url('fonts/icomoon.woff?lqk80d') format('woff'),
url('fonts/icomoon.svg?lqk80d#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
[class^="ico-"], [class*=" ico-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icomoon' !important;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
cursor: pointer;
font-size: 13px;
line-height: 20px;
}
.ico-qqz:before {
content: "\e900";
}
.ico-find:before {
content: "\e602";
}
.ico-findfiles:before {
content: "\e603";
}
.ico-editor:before {
content: "\e604";
}
.ico-notification:before {
content: "\e607";
}
.ico-price:before {
content: "\e616";
}
.ico-report:before {
content: "\e605";
}
.ico-git:before {
content: "\e624";
}
.ico-book:before {
content: "\e623";
}
.ico-start:before {
content: "\e9d7";
text-shadow: 0 0 rgba(0, 0, 0, 0.4);
}
.ico-tree:before {
content: "\e600";
}
.ico-build:before {
content: "\e601";
}
.ico-export:before {
content: "\f0ed";
}
.ico-import:before {
content: "\f0ee";
}
.ico-keyboard:before {
content: "\f11c";
}
.ico-moveup:before {
content: "\f148";
}
.ico-movedown:before {
content: "\f149";
}
.ico-weibo:before {
content: "\e621";
}
.ico-uniE608:before {
content: "\e608";
}
.ico-max:before {
content: "\e609";
}
.ico-remove:before {
content: "\e60b";
}
.ico-buildrun:before {
content: "\e60c";
}
.ico-about:before {
content: "\e60d";
}
.ico-undo:before {
content: "\e60e";
}
.ico-stop:before {
content: "\e60f";
}
.ico-close:before {
content: "\e611";
text-shadow: 0 0 rgba(0, 0, 0, 0.4);
}
.ico-format:before {
content: "\e612";
}
.ico-restore:before {
content: "\e613";
}
.toolbars .ico-restore:before {
content: "\e60a";
}
.ico-min:before {
content: "\e614";
position: absolute;
right: 5px;
}
.ico-redo:before {
content: "\e615";
}
.ico-uniE617:before {
content: "\e617";
}
.ico-signout:before {
content: "\e618";
}
.ico-email:before {
content: "\e619";
}
.ico-googleplus:before {
content: "\e61a";
}
.ico-facebook:before {
content: "\e61b";
}
.ico-twitter:before {
content: "\e61c";
}
.ico-info:before {
content: "\e61d";
}
.ico-goline:before {
content: "\e61e";
}
.ico-share:before {
content: "\e61f";
}
.ico-comment:before {
content: "\e620";
}
.ico-github:before {
content: "\f00a";
}
.ico-refresh:before {
content: "\f021";
}
.ico-save:before {
content: "\f0c7";
}
/* end ico */

View File

@ -1,23 +1,7 @@
/*
* Copyright (c) 2014-present, b3log.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** /**
* dialig style * dialig style
* *
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a> * @author <a href="mailto:LLY219@gmail.com">Liyuan Li</a>
* @version 0.0.0.6, Jun 3, 2012 * @version 0.0.0.6, Jun 3, 2012
*/ */
@ -40,21 +24,23 @@
display: none; display: none;
-moz-user-select: none; -moz-user-select: none;
user-select: none; user-select: none;
box-shadow: 0 2px 10px 1px #000000;
} }
.dialog-title { .dialog-title {
float: left; float: left;
line-height: 22px; line-height: 22px;
margin-left: 3px; margin-left: 3px;
font-weight: bold; color: #000;
} }
.dialog-header-bg { .dialog-header-bg {
height: 23px; height: 23px;
background-color: #bbb; background-color: #CCD5E5;
cursor: move; cursor: move;
width: 100%; width: 100%;
border: 1px solid #9B9B9B;
border-top-color: #8E97A7;
border-bottom-color: #8891A1;
} }
.dialog-close-icon { .dialog-close-icon {
@ -63,8 +49,10 @@
text-decoration: none; text-decoration: none;
} }
.dialog-close-icon:hover { .dialog-main {
text-decoration: none; border: 1px solid #9B9B9B;
border-top-width: 0px;
background-color: #F0F0F0;
} }
.dialog-main > div { .dialog-main > div {
@ -76,8 +64,7 @@
text-align: right; text-align: right;
} }
.dialog-footer button, .dialog-footer button {
#dialogCloseEditor button {
margin: 0 5px; margin: 0 5px;
} }
@ -89,34 +76,7 @@
overflow: hidden; overflow: hidden;
} }
.dialog-main input, .dialog-main input {
.dialog-main select {
width: 100%; width: 100%;
margin: 2px auto; margin: 2px auto;
}
#dialogGoFilePrompt > ul {
position: relative;
height: 260px;
overflow: auto;
margin-top: 5px;
background-color: #FFF;
border: 1px solid #919191;
}
#dialogPreference {
margin: 10px;
}
#dialogPreference .tabs-panel {
padding: 10px;
}
#dialogPreference .preference {
margin-bottom: 10px;
}
#dialogPreference img.gravatar {
width: 48px;
height: 48px;
} }

BIN
static/css/fonts/icomoon.eot Executable file → Normal file

Binary file not shown.

72
static/css/fonts/icomoon.svg Executable file → Normal file
View File

@ -3,53 +3,27 @@
<svg xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata> <metadata>Generated by IcoMoon</metadata>
<defs> <defs>
<font id="icomoon" horiz-adv-x="1024"> <font id="icomoon" horiz-adv-x="512">
<font-face units-per-em="1024" ascent="960" descent="-64" /> <font-face units-per-em="512" ascent="480" descent="-32" />
<missing-glyph horiz-adv-x="1024" /> <missing-glyph horiz-adv-x="512" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" /> <glyph unicode="&#x20;" d="" horiz-adv-x="256" />
<glyph unicode="&#xe600;" glyph-name="tree" horiz-adv-x="1170" d="M0 886.858c0 40.396 32.747 73.143 73.143 73.143s73.143-32.747 73.143-73.143c0-40.396-32.747-73.143-73.143-73.143s-73.143 32.747-73.143 73.143zM292.572 960h877.714v-146.286h-877.714zM292.572 594.286c0 40.396 32.747 73.143 73.143 73.143s73.143-32.747 73.143-73.143c0-40.396-32.747-73.143-73.143-73.143s-73.143 32.747-73.143 73.143zM585.142 667.428h585.144v-146.286h-585.144zM292.572 9.142c0 40.396 32.747 73.143 73.143 73.143s73.143-32.747 73.143-73.143c0-40.396-32.747-73.143-73.143-73.143s-73.143 32.747-73.143 73.143zM585.142 82.286h585.144v-146.286h-585.144zM585.142 301.714c0 40.396 32.747 73.143 73.143 73.143s73.143-32.747 73.143-73.143c0-40.396-32.747-73.143-73.143-73.143s-73.143 32.747-73.143 73.143zM877.714 374.858h292.572v-146.286h-292.572z" /> <glyph unicode="&#xe600;" d="M219.464 448.036q7.428 0 12.857-5.428t5.429-12.857v-64h84l49.428 49.428q5.428 5.428 12.857 5.428t12.857-5.428 5.428-12.857-5.428-12.857l-49.428-49.428v-241.143l49.428-49.428q5.428-5.428 5.428-12.857t-5.428-12.857-12.857-5.428-12.857 5.428l-49.428 49.428h-84v-64q0-7.428-5.428-12.857t-12.857-5.428-12.857 5.428-5.428 12.857v64q-48.857 0-82.857 19.143l-59.715-59.428q-5.428-5.428-12.857-5.428t-12.857 5.428q-5.428 5.143-5.428 12.857t5.428 12.857l56.285 56.572q-1.428 1.428-3.715 4.286t-8.143 12-10.428 18.572-8.285 23.428-3.715 27.714h256v36.572h-256q0 14.572 3.857 29t9.428 24.857 11.143 18.857 9.285 12.428l4 4.286-59.143 52.285q-6 5.715-6 13.715 0 6.857 4.572 12.285 5.143 5.429 12.715 5.857t13.285-4.428l64.857-57.714q32.572 16.571 78.285 16.571v64q0 7.429 5.429 12.857t12.857 5.428zM384.036 310.893q38 0 64.714-26.714t26.714-64.714-26.714-64.714-64.714-26.714v182.857z" horiz-adv-x="476" />
<glyph unicode="&#xe601;" glyph-name="build" d="M1009.996 131.024l-301.544 301.544c-18.668 18.668-49.214 18.668-67.882 0l-22.626-22.626-184 184 302.056 302.058h-320l-142.058-142.058-14.060 14.058h-67.882v-67.882l14.058-14.058-206.058-206.060 160-160 206.058 206.058 184-184-22.626-22.626c-18.668-18.668-18.668-49.214 0-67.882l301.544-301.544c18.668-18.668 49.214-18.668 67.882 0l113.136 113.136c18.67 18.666 18.67 49.214 0.002 67.882z" /> <glyph unicode="&#xe601;" d="M512 480v-192l-69.13 69.13-106-106-53.74 53.74 106 106-69.13 69.13zM122.87 410.87l106-106-53.74-53.74-106 106-69.13-69.13v192h192zM442.87 90.87l69.13 69.13v-192h-192l69.13 69.13-106 106 53.74 53.74zM228.87 143.13l-106-106 69.13-69.13h-192v192l69.13-69.13 106 106z" />
<glyph unicode="&#xe602;" glyph-name="find" d="M632.576 661.376c46.080-10.666 82.752-49.92 93.418-92.8 3.648-20.054 21.12-35.2 42.048-35.2 23.466 0 42.666 19.2 42.666 42.666 0 30.72-25.814 77.44-61.014 112.426-34.56 34.134-75.734 58.24-109.654 58.24-23.466 0-42.666-19.84-42.666-43.306 0.002-20.884 15.148-38.4 35.202-42.026zM46.122 102.016c-33.066-33.046-33.066-86.826 0-119.872 33.066-33.088 86.826-33.088 119.68 0l245.098 244.906c57.194-35.67 124.586-56.342 197.12-56.342 206.294 0 373.334 167.040 373.334 373.334s-167.040 373.334-373.334 373.334-373.334-167.040-373.334-373.334c0-72.32 20.714-139.946 56.342-197.12l-244.906-244.906zM341.376 544.042c0 147.2 119.488 266.666 266.666 266.666 147.2 0 266.666-119.466 266.666-266.666s-119.466-266.666-266.666-266.666c-147.178 0.022-266.666 119.488-266.666 266.666z" /> <glyph unicode="&#xe602;" d="M32 192h192v-192l-69.13 69.13-101-101-53.74 53.74 101 101zM410.87 122.87l101-101-53.74-53.74-101 101-69.13-69.13v192h192zM480 256h-192v192l69.13-69.13 101 101 53.74-53.74-101-101zM154.87 378.87l69.13 69.13v-192h-192l69.13 69.13-101 101 53.74 53.74z" />
<glyph unicode="&#xe603;" glyph-name="findfiles" d="M0 21.334c0-47.146 38.186-85.334 85.334-85.334h277.334c47.146 0 85.334 38.186 85.334 85.334l-0.002 341.332h128v-341.334c0-47.146 38.186-85.334 85.334-85.334h277.334c47.146 0 85.334 38.186 85.334 85.334l-0.002 554.668c0 47.146-38.186 85.334-85.334 85.334h-42.666v192h10.666c29.44 0 53.334 23.892 53.334 53.332s-23.894 53.334-53.334 53.334h-277.334c-29.438 0-53.332-23.894-53.332-53.334s23.894-53.332 53.334-53.332h10.666v-192h-256v192h10.666c29.44 0 53.334 23.892 53.334 53.332s-23.894 53.334-53.334 53.334h-277.334c-29.438 0-53.332-23.894-53.332-53.334s23.894-53.332 53.334-53.332h10.666v-192h-42.666c-47.146 0-85.334-38.186-85.334-85.334v-554.666zM554.666 469.334h-85.334c-23.466 0-42.666 19.2-42.666 42.666s19.2 42.666 42.666 42.666h85.334c23.466 0 42.666-19.2 42.666-42.666s-19.198-42.666-42.666-42.666z" /> <glyph unicode="&#xe603;" d="M504.998 65.512l-150.772 150.772c-9.334 9.334-24.607 9.334-33.941 0l-11.313-11.313-92 92 151.028 151.029h-160l-71.029-71.029-7.030 7.029h-33.941v-33.941l7.029-7.029-103.029-103.030 80-80 103.029 103.029 92-92-11.313-11.313c-9.334-9.334-9.334-24.607 0-33.941l150.772-150.772c9.334-9.334 24.607-9.334 33.941 0l56.568 56.568c9.335 9.333 9.335 24.607 0.001 33.941z" />
<glyph unicode="&#xe604;" glyph-name="editor" d="M118.634 528.918c-20.886-20.886-30.934-50.774-36.032-81.066l-81.322-384.428c-11.52-68.288 56.96-136.982 126.102-126.314l384.874 80.234c28.16 5.098 58.666 13.866 79.574 34.774l407.68 408.128c32.64 32.832 32.64 85.952 0 118.806l-356.054 356.48c-32.854 32.64-85.974 32.64-118.634 0l-406.188-406.614zM509.888 118.678l-199.254-38.208-165.12 165.334 37.952 199.488 296.768 297.174c16.426 16.448 42.88 16.448 59.306 0 16.448-16.426 16.448-43.094 0-59.52l-274.368-274.774c-20.48-20.48-20.48-53.782 0-74.218 20.48-20.502 53.546-20.502 74.026 0l274.582 274.774c16.448 16.426 42.88 16.426 59.306 0 16.448-16.448 16.448-42.902 0-59.306l-274.368-274.774c-20.48-20.48-20.48-53.782 0-74.24 20.48-20.48 53.568-20.48 74.026 0l274.582 274.774c16.234 16.234 42.902 16.234 59.328 0 16.426-16.426 16.426-43.094 0-59.498l-296.766-297.006z" /> <glyph unicode="&#xe604;" d="M400 416h-288c-26.51 0-48-21.49-48-48v-16h384v16c0 26.51-21.49 48-48 48zM316.16 448l7.058-50.5h-134.436l7.057 50.5h120.321zM320 480h-128c-13.2 0-25.495-10.696-27.321-23.769l-9.357-66.962c-1.827-13.073 7.478-23.769 20.678-23.769h160c13.2 0 22.505 10.696 20.679 23.769l-9.357 66.962c-1.827 13.073-14.122 23.769-27.322 23.769v0zM408 320h-304c-17.6 0-30.696-14.341-29.103-31.869l26.206-288.263c1.593-17.527 17.297-31.868 34.897-31.868h240c17.6 0 33.304 14.341 34.897 31.868l26.205 288.263c1.594 17.528-11.502 31.869-29.102 31.869zM192 32h-48l-16 224h64v-224zM288 32h-64v224h64v-224zM368 32h-48v224h64l-16-224z" />
<glyph unicode="&#xe605;" glyph-name="report" d="M622.858 799.808l-22.032 110.158h-495.715v-936.349h110.158v385.556h308.443l22.032-110.158h385.556v550.795z" /> <glyph unicode="&#xe605;" d="M96 416l320-192-320-192z" />
<glyph unicode="&#xe606;" glyph-name="signup" d="M697.83 494.458c76.887 0 138.908 62.486 138.908 139.372s-62.021 139.372-138.908 139.372c-76.887 0-139.372-62.486-139.372-139.372s62.486-139.372 139.372-139.372zM326.17 494.458c76.887 0 138.908 62.486 138.908 139.372s-62.021 139.372-138.908 139.372c-76.887 0-139.372-62.486-139.372-139.372s62.486-139.372 139.372-139.372zM326.17 401.542c-108.479 0-325.203-54.355-325.203-162.601v-116.144h650.404v116.144c0 108.245-216.724 162.601-325.203 162.601zM697.83 401.542c-13.472 0-28.571-0.929-44.831-2.555 53.89-38.792 91.289-91.057 91.289-160.047v-116.144h278.744v116.144c0 108.245-216.724 162.601-325.203 162.601z" /> <glyph unicode="&#xe606;" d="M360.704 429.209c-29.209 21.069-65.843 31.591-109.978 31.591-33.587 0-61.901-7.424-84.915-22.221-36.531-23.194-55.936-62.566-58.291-118.118h84.633c0 16.179 4.71 31.769 14.157 46.771s25.472 22.502 48.077 22.502c22.963 0 38.81-6.093 47.462-18.253 8.678-12.211 13.005-25.702 13.005-40.499 0-12.877-6.451-24.653-14.233-35.379-4.275-6.247-9.933-11.981-16.921-17.255 0 0-45.901-29.44-66.073-53.095-11.699-13.722-12.749-34.253-13.773-63.719-0.077-2.099 0.717-6.425 8.064-6.425s59.315 0 65.843 0 7.885 4.839 7.987 6.963c0.461 10.726 1.664 16.205 3.635 22.4 3.712 11.699 13.747 21.913 25.063 30.695l23.296 16.077c21.017 16.384 37.811 29.824 45.209 40.371 12.647 17.357 21.529 38.707 21.529 64.025 0 41.344-14.618 72.525-43.776 93.568zM249.369 104.345c-29.184 0.87-53.248-19.303-54.169-50.944-0.922-31.616 21.965-52.505 51.149-53.376 30.464-0.896 53.888 18.637 54.81 50.253 0.896 31.642-21.325 53.171-51.789 54.067z" />
<glyph unicode="&#xe607;" glyph-name="notification" d="M977.76 152.992l-400.608 685.952c-33.408 33.408-87.616 33.408-121.024 0l-400.64-685.952c-33.408-33.376-33.408-87.552 0-120.992h922.24c33.472 33.44 33.472 87.616 0.032 120.992zM479.744 592.704c0 26.528 21.504 48 48 48s48-21.472 48-48v-224c0-26.496-21.504-48-48-48s-48 21.504-48 48v224zM528.032 160.448c-26.496 0-48 21.44-48 48 0 26.496 21.504 48 48 48s48-21.504 48-48c0-26.56-21.504-48-48-48z" /> <glyph unicode="&#xe607;" d="M80.231 302.016c23.91 18.56 43.725 5.786 70.169-24.832 2.995-3.43 6.989 0.589 9.242 2.56 2.253 1.997 37.171 33.382 38.861 34.841 1.715 1.51 3.763 4.327 1.049 7.475s-12.647 16-19.021 24.32c-46.285 60.544 126.618 101.607 100.045 102.247-13.491 0.358-67.738 0.973-75.827 0.128-32.845-3.481-74.087-34.176-94.848-48.461-27.136-18.662-37.299-29.491-38.963-31.001-7.68-6.733-1.229-22.195-15.155-34.406-14.72-12.902-23.885-3.149-32.41-10.624-4.25-3.738-16.051-12.57-19.456-15.565-3.379-2.969-3.994-8.013-0.538-12.058 0 0 32.333-35.737 35.072-38.886 2.714-3.149 10.009-5.811 14.541-1.817 4.531 3.968 16.154 14.183 18.125 15.923 1.997 1.715-1.28 22.093 9.114 30.157zM226.381 288.807c-3.072 3.558-6.861 3.635-10.163 0.717l-36.736-32.025c-2.867-2.56-3.251-7.271-0.666-10.24l212.327-241.613c4.941-5.735 13.543-6.323 19.251-1.357l24.858 20.787c5.658 4.966 6.247 13.671 1.305 19.379l-210.176 244.352zM509.491 391.642c-1.894 12.647-8.448 9.984-11.853 4.608-3.353-5.351-18.457-28.186-24.653-38.502-6.144-10.291-21.248-30.489-49.51-10.547-29.389 20.787-19.149 35.303-14.055 45.056 5.146 9.805 20.941 37.248 23.245 40.704 2.253 3.456-0.409 13.491-9.498 9.293-9.139-4.224-64.589-26.265-72.295-57.856-7.859-32.179 6.605-60.903-21.76-89.447l-34.355-35.84 34.509-40.141 42.342 40.218c10.112 10.137 31.642 19.994 51.149 15.565 41.805-9.472 64.589 6.247 78.361 32.179 12.339 23.219 10.291 72.064 8.371 84.71zM70.144 41.894c-5.325-5.376-5.325-14.080 0-19.43l24.346-23.808c5.325-5.351 13.773-3.098 19.097 2.253l125.619 123.52-38.502 43.853-130.56-126.387z" />
<glyph unicode="&#xe608;" glyph-name="uniE608" horiz-adv-x="952" d="M438.928 896.072q14.856 0 25.714-10.856t10.858-25.714v-128h168l98.856 98.856q10.856 10.856 25.714 10.856t25.714-10.856 10.856-25.714-10.856-25.714l-98.856-98.856v-482.286l98.856-98.856q10.856-10.856 10.856-25.714t-10.856-25.714-25.714-10.856-25.714 10.856l-98.856 98.856h-168v-128q0-14.856-10.856-25.714t-25.714-10.856-25.714 10.856-10.856 25.714v128q-97.714 0-165.714 38.286l-119.43-118.856q-10.856-10.856-25.714-10.856t-25.714 10.856q-10.856 10.286-10.856 25.714t10.856 25.714l112.57 113.144q-2.856 2.856-7.43 8.572t-16.286 24-20.856 37.144-16.57 46.856-7.43 55.428h512v73.144h-512q0 29.144 7.714 58t18.856 49.714 22.286 37.714 18.57 24.856l8 8.572-118.286 104.57q-12 11.43-12 27.43 0 13.714 9.144 24.57 10.286 10.858 25.43 11.714t26.57-8.856l129.714-115.428q65.144 33.142 156.57 33.142v128q0 14.858 10.858 25.714t25.714 10.856zM768.072 621.786q76 0 129.428-53.428t53.428-129.428-53.428-129.428-129.428-53.428v365.714z" /> <glyph unicode="&#xe608;" d="M64 416h384v-384h-384z" />
<glyph unicode="&#xe609;" glyph-name="max" d="M1024 960v-384l-138.26 138.26-212-212-107.48 107.48 212 212-138.26 138.26zM245.74 821.74l212-212-107.48-107.48-212 212-138.26-138.26v384h384zM885.74 181.74l138.26 138.26v-384h-384l138.26 138.26-212 212 107.48 107.48zM457.74 286.26l-212-212 138.26-138.26h-384v384l138.26-138.26 212 212z" /> <glyph unicode="&#xe609;" d="M181.861 118.974l20.649 28.908-22.627 22.628-28.909-20.648c-5.361 2.997-11.102 5.387-17.133 7.096l-5.841 35.042h-32l-5.84-35.043c-6.031-1.709-11.772-4.099-17.133-7.096l-28.909 20.649-22.628-22.628 20.649-28.908c-2.997-5.36-5.387-11.103-7.096-17.133l-35.043-5.841v-32l35.043-5.841c1.709-6.030 4.099-11.772 7.096-17.133l-20.649-28.908 22.627-22.628 28.909 20.648c5.361-2.997 11.102-5.387 17.133-7.096l5.841-35.042h32l5.84 35.043c6.031 1.709 11.772 4.099 17.133 7.096l28.909-20.648 22.627 22.628-20.649 28.908c2.997 5.36 5.387 11.103 7.096 17.133l35.044 5.84v32l-35.043 5.841c-1.709 6.030-4.099 11.772-7.096 17.133zM112 48c-17.674 0-32 14.327-32 32s14.326 32 32 32 32-14.327 32-32-14.326-32-32-32zM512 288v32l-33.691 6.125c-0.621 4.023-1.416 7.989-2.362 11.895l28.779 18.55-12.246 29.564-33.472-7.234c-2.107 3.455-4.363 6.81-6.746 10.065l19.503 28.171-22.628 22.627-28.171-19.503c-3.256 2.383-6.61 4.638-10.065 6.747l7.234 33.472-29.564 12.247-18.55-28.779c-3.906 0.946-7.872 1.741-11.895 2.362l-6.126 33.691h-32l-6.126-33.691c-4.023-0.621-7.988-1.416-11.895-2.362l-18.549 28.779-29.564-12.246 7.234-33.472c-3.455-2.108-6.81-4.364-10.065-6.747l-28.171 19.503-22.627-22.627 19.503-28.171c-2.383-3.255-4.639-6.61-6.747-10.065l-33.472 7.234-12.246-29.564 28.779-18.55c-0.946-3.906-1.741-7.871-2.362-11.895l-33.692-6.126v-32l33.691-6.125c0.621-4.023 1.416-7.989 2.362-11.895l-28.779-18.55 12.246-29.564 33.472 7.234c2.108-3.455 4.364-6.809 6.747-10.065l-19.503-28.171 22.627-22.628 28.171 19.503c3.255-2.383 6.61-4.638 10.065-6.746l-7.234-33.472 29.564-12.246 18.551 28.779c3.905-0.946 7.871-1.741 11.894-2.362l6.126-33.692h32l6.126 33.691c4.022 0.621 7.988 1.416 11.895 2.362l18.55-28.779 29.564 12.246-7.234 33.472c3.455 2.108 6.81 4.363 10.065 6.746l28.171-19.503 22.628 22.628-19.503 28.171c2.383 3.256 4.638 6.61 6.746 10.065l33.472-7.234 12.246 29.565-28.779 18.55c0.946 3.906 1.741 7.871 2.362 11.895l33.691 6.125zM336 234.4c-38.439 0-69.6 31.161-69.6 69.6s31.16 69.6 69.6 69.6 69.6-31.161 69.6-69.6c0-38.439-31.16-69.6-69.6-69.6z" />
<glyph unicode="&#xe60a;" glyph-name="uniE60A" d="M64 384h384v-384l-138.26 138.26-202-202-107.48 107.48 202 202zM821.74 245.74l202-202-107.48-107.48-202 202-138.26-138.26v384h384zM960 512h-384v384l138.26-138.26 202 202 107.48-107.48-202-202zM309.74 757.74l138.26 138.26v-384h-384l138.26 138.26-202 202 107.48 107.48z" /> <glyph unicode="&#xe60a;" d="M367.334 149.709l-70.605 80.691 70.605 80.691c12.007 12.007 12.007 31.463 0 43.443s-31.462 11.981-43.443 0l-67.891-77.593-67.865 77.568c-12.006 12.006-31.463 12.006-43.443 0s-11.981-31.463 0-43.443l70.579-80.666-70.605-80.691c-11.981-12.007-11.981-31.411 0-43.392 12.006-12.007 31.463-12.007 43.443 0l67.891 77.543 67.865-77.543c12.007-12.007 31.462-12.007 43.443 0s12.007 31.385 0.026 43.392z" />
<glyph unicode="&#xe60b;" glyph-name="remove" d="M800 832h-576c-53.020 0-96-42.98-96-96v-32h768v32c0 53.020-42.98 96-96 96zM632.32 896l14.116-101h-268.872l14.114 101h240.642zM640 960h-256c-26.4 0-50.99-21.392-54.642-47.538l-18.714-133.924c-3.654-26.146 14.956-47.538 41.356-47.538h320c26.4 0 45.010 21.392 41.358 47.538l-18.714 133.924c-3.654 26.146-28.244 47.538-54.644 47.538v0zM816 640h-608c-35.2 0-61.392-28.682-58.206-63.738l52.412-576.526c3.186-35.054 34.594-63.736 69.794-63.736h480c35.2 0 66.608 28.682 69.794 63.736l52.41 576.526c3.188 35.056-23.004 63.738-58.204 63.738zM384 64h-96l-32 448h128v-448zM576 64h-128v448h128v-448zM736 64h-96v448h128l-32-448z" /> <glyph unicode="&#xe60b;" d="M224 82.745l-102.627 118.627 29.254 29.255 73.373-57.372 137.372 121.372 29.256-29.254zM415.886 416c0.039-0.033 0.081-0.075 0.114-0.115v-383.771c-0.033-0.039-0.075-0.081-0.114-0.114h-319.772c-0.040 0.033-0.081 0.075-0.114 0.114v383.772c0.033 0.040 0.075 0.081 0.115 0.114h-64.115v-384c0-35.2 28.8-64 64-64h320c35.2 0 64 28.8 64 64v384h-64.114zM320 416v32c0 17.673-14.327 32-32 32h-64c-17.673 0-32-14.327-32-32v-32h-64v-64h256v64h-64zM288 416h-64v32h64v-32z" />
<glyph unicode="&#xe60c;" glyph-name="buildrun" d="M192 832l640-384-640-384z" /> <glyph unicode="&#xe60c;" d="M64 341.333v64h448v-320h-64v-64h-448v320h64zM448 128h21.334v234.667h-362.666v-21.333h341.334v-213.334zM42.666 64h362.666v234.667h-362.666v-234.667z" />
<glyph unicode="&#xe60d;" glyph-name="about" d="M708.524 833.167c-54.823 39.545-123.584 59.295-206.423 59.295-63.041 0-116.185-13.935-159.382-41.708-68.567-43.534-104.989-117.434-109.41-221.702h158.852c0 30.367 8.841 59.629 26.572 87.787s47.81 42.236 90.238 42.236c43.101 0 72.844-11.436 89.084-34.26 16.289-22.92 24.41-48.242 24.41-76.015 0-24.169-12.108-46.272-26.714-66.405-8.024-11.726-18.644-22.488-31.759-32.387 0 0-86.154-55.258-124.016-99.657-21.959-25.755-23.929-64.291-25.851-119.598-0.144-3.94 1.346-12.059 15.136-12.059s111.332 0 123.584 0 14.8 9.083 14.991 13.069c0.865 20.133 3.123 30.416 6.823 42.044 6.967 21.959 25.803 41.129 47.042 57.614l43.726 30.176c39.447 30.752 70.97 55.978 84.855 75.775 23.738 32.578 40.409 72.651 40.409 120.171 0 77.601-27.437 136.126-82.166 175.623zM499.553 223.413c-54.777 1.633-99.944-36.231-101.672-95.619-1.73-59.342 41.227-98.549 96.004-100.184 57.18-1.682 101.145 34.981 102.875 94.322 1.682 59.39-40.026 99.8-97.205 101.481z" /> <glyph unicode="&#xe60d;" d="M0 272v-96c0-8.836 7.164-16 16-16h480c8.836 0 16 7.164 16 16v96c0 8.836-7.164 16-16 16h-480c-8.836 0-16-7.164-16-16z" />
<glyph unicode="&#xe60e;" glyph-name="undo" d="M761.862-64c113.726 206.032 132.888 520.306-313.862 509.824v-253.824l-384 384 384 384v-248.372c534.962 13.942 594.57-472.214 313.862-775.628z" /> <glyph unicode="&#xf021;" d="M431.714 178.286q0-1.428-0.286-2-18.286-76.572-76.572-124.143t-136.571-47.572q-41.715 0-80.715 15.714t-69.572 44.857l-36.857-36.857q-5.428-5.428-12.857-5.428t-12.857 5.428-5.428 12.857v128q0 7.428 5.428 12.857t12.857 5.428h128q7.428 0 12.857-5.428t5.429-12.857-5.428-12.857l-39.143-39.143q20.285-18.857 46-29.143t53.429-10.286q38.286 0 71.428 18.572t53.143 51.143q3.143 4.857 15.143 33.428 2.286 6.572 8.572 6.572h54.857q3.714 0 6.428-2.714t2.714-6.428zM438.857 406.857v-128q0-7.428-5.428-12.857t-12.857-5.428h-128q-7.428 0-12.857 5.428t-5.428 12.857 5.428 12.857l39.428 39.428q-42.286 39.143-99.714 39.143-38.286 0-71.428-18.571t-53.143-51.143q-3.143-4.857-15.143-33.429-2.285-6.572-8.572-6.572h-56.857q-3.715 0-6.428 2.714t-2.715 6.428v2q18.572 76.572 77.143 124.143t137.143 47.571q41.714 0 81.143-15.857t70-44.714l37.143 36.857q5.428 5.428 12.857 5.428t12.857-5.428 5.428-12.857z" />
<glyph unicode="&#xe60f;" glyph-name="stop" d="M128 832h768v-768h-768z" /> <glyph unicode="&#xf05b;" d="M342 187.428h-31.143q-7.428 0-12.857 5.428t-5.428 12.857v36.572q0 7.428 5.428 12.857t12.857 5.429h31.143q-9.143 30.857-32.143 53.857t-53.857 32.143v-31.143q0-7.429-5.429-12.857t-12.857-5.428h-36.571q-7.428 0-12.857 5.428t-5.429 12.857v31.143q-30.857-9.143-53.857-32.143t-32.143-53.857h31.143q7.429 0 12.857-5.428t5.428-12.857v-36.571q0-7.428-5.428-12.857t-12.857-5.428h-31.143q9.143-30.857 32.143-53.857t53.857-32.143v31.143q0 7.428 5.428 12.857t12.857 5.428h36.571q7.428 0 12.857-5.428t5.429-12.857v-31.143q30.857 9.143 53.857 32.143t32.143 53.857zM438.857 242.286v-36.572q0-7.428-5.428-12.857t-12.857-5.428h-40.857q-10.572-46-44.143-79.572t-79.572-44.143v-40.857q0-7.428-5.428-12.857t-12.857-5.428h-36.571q-7.428 0-12.857 5.428t-5.429 12.857v40.857q-46 10.572-79.572 44.143t-44.143 79.572h-40.857q-7.428 0-12.857 5.428t-5.428 12.857v36.572q0 7.428 5.428 12.857t12.857 5.429h40.857q10.572 46 44.143 79.572t79.572 44.143v40.857q0 7.428 5.428 12.857t12.857 5.428h36.571q7.428 0 12.857-5.428t5.429-12.857v-40.857q46-10.571 79.572-44.143t44.143-79.572h40.857q7.428 0 12.857-5.428t5.428-12.857z" />
<glyph unicode="&#xe611;" glyph-name="close" d="M734.668 299.418l-141.21 161.382 141.21 161.382c24.014 24.014 24.014 62.926 0 86.886s-62.924 23.962-86.886 0l-135.782-155.186-135.73 155.136c-24.012 24.012-62.926 24.012-86.886 0s-23.962-62.926 0-86.886l141.158-161.332-141.21-161.382c-23.962-24.014-23.962-62.822 0-86.784 24.012-24.014 62.926-24.014 86.886 0l135.782 155.086 135.73-155.086c24.014-24.014 62.924-24.014 86.886 0s24.014 62.77 0.052 86.784z" /> <glyph unicode="&#xf096;" d="M320 390.857h-237.714q-18.857 0-32.285-13.428t-13.428-32.285v-237.715q0-18.857 13.428-32.286t32.285-13.428h237.714q18.857 0 32.286 13.428t13.428 32.286v237.714q0 18.857-13.428 32.286t-32.286 13.428zM402.286 345.143v-237.715q0-34-24.143-58.143t-58.143-24.143h-237.714q-34 0-58.143 24.143t-24.143 58.143v237.714q0 34 24.143 58.143t58.143 24.143h237.714q34 0 58.143-24.143t24.143-58.143z" horiz-adv-x="403" />
<glyph unicode="&#xe612;" glyph-name="format" d="M448 165.49l-205.254 237.254 58.508 58.51 146.746-114.744 274.744 242.744 58.512-58.508zM831.772 832c0.078-0.066 0.162-0.15 0.228-0.23v-767.542c-0.066-0.078-0.15-0.162-0.228-0.228h-639.544c-0.080 0.066-0.162 0.15-0.228 0.228v767.544c0.066 0.080 0.15 0.162 0.23 0.228h-128.23v-768c0-70.4 57.6-128 128-128h640c70.4 0 128 57.6 128 128v768h-128.228zM640 832v64c0 35.346-28.654 64-64 64h-128c-35.346 0-64-28.654-64-64v-64h-128v-128h512v128h-128zM576 832h-128v64h128v-64z" /> <glyph unicode="&#xf0c7;" d="M109.715 41.143h219.429v109.714h-219.429v-109.714zM365.714 41.143h36.572v256q0 4-2.857 11t-5.714 9.857l-80.286 80.285q-2.857 2.857-9.714 5.715t-11.143 2.857v-118.857q0-11.429-8-19.429t-19.428-8h-164.571q-11.428 0-19.428 8t-8 19.428v118.857h-36.572v-365.715h36.572v118.857q0 11.428 8 19.428t19.428 8h237.715q11.428 0 19.428-8t8-19.428v-118.857zM256 306.286v91.428q0 3.714-2.714 6.429t-6.428 2.714h-54.857q-3.714 0-6.429-2.714t-2.714-6.429v-91.428q0-3.714 2.714-6.428t6.428-2.714h54.857q3.714 0 6.429 2.714t2.714 6.428zM438.857 297.143v-265.143q0-11.428-8-19.428t-19.428-8h-384q-11.428 0-19.428 8t-8 19.428v384q0 11.428 8 19.428t19.428 8h265.143q11.428 0 25.143-5.714t21.714-13.714l80-80q8-8 13.714-21.714t5.714-25.143z" />
<glyph unicode="&#xe613;" glyph-name="restore" d="M128 682.666v128h896v-640h-128v-128h-896v640h128zM896 256h42.668v469.334h-725.332v-42.666h682.668v-426.668zM85.332 128h725.332v469.334h-725.332v-469.334z" /> <glyph unicode="&#xf0e5;" d="M256 370.286q-58.286 0-109-19.857t-80.572-53.572-29.857-72.857q0-32 20.428-61t57.572-50.143l24.857-14.286-7.714-27.428q-6.857-26-20-49.143 43.428 18 78.572 48.857l12.286 10.857 16.286-1.714q19.714-2.286 37.143-2.286 58.285 0 109 19.857t80.572 53.572 29.857 72.857-29.857 72.857-80.572 53.572-109 19.857zM512 224q0-49.714-34.286-91.857t-93.143-66.572-128.572-24.428q-20 0-41.428 2.286-56.572-50-131.429-69.143-14-4-32.572-6.286h-1.428q-4.285 0-7.715 3t-4.572 7.857v0.286q-0.857 1.143-0.143 3.428t0.572 2.857 1.285 2.714l1.715 2.572t2 2.428 2.285 2.572q2 2.286 8.857 9.857t9.857 10.857 8.857 11.286 9.285 14.572 7.715 16.857 7.428 21.714q-44.857 25.428-70.715 62.857t-25.857 80.286q0 49.714 34.285 91.857t93.143 66.571 128.571 24.429 128.571-24.429 93.143-66.571 34.286-91.857z" />
<glyph unicode="&#xe614;" glyph-name="min" d="M0 544v-192c0-17.672 14.328-32 32-32h960c17.672 0 32 14.328 32 32v192c0 17.672-14.328 32-32 32h-960c-17.672 0-32-14.328-32-32z" />
<glyph unicode="&#xe615;" glyph-name="redo" d="M576 711.628v248.372l384-384-384-384v253.824c-446.75 10.482-427.588-303.792-313.86-509.824-280.712 303.414-221.1 789.57 313.86 775.628z" />
<glyph unicode="&#xe616;" glyph-name="price" d="M501.443 506.062c-119.819 31.143-158.353 63.078-158.353 113.223 0 57.535 53.047 97.916 142.518 97.916 93.957 0 128.794-44.867 131.961-110.848h116.654c-3.431 91.053-59.119 173.925-169.439 201.109v115.599h-158.353v-114.015c-102.402-22.433-184.746-88.413-184.746-190.553 0-121.933 101.082-182.636 248.086-217.999 132.225-31.671 158.353-77.856 158.353-127.474 0-36.159-25.601-94.219-142.518-94.219-108.736 0-151.755 48.826-157.297 110.848h-116.389c6.597-115.599 92.901-180.26 194.511-202.165v-114.544h158.353v113.487c102.667 19.794 184.746 79.178 184.746 187.65 0 149.38-128.266 200.581-248.086 231.987z" />
<glyph unicode="&#xe617;" glyph-name="uniE617" d="M384 448h-320v128h320v128l192-192-192-192zM1024 960v-832l-384-192v192h-384v256h64v-192h320v576l256 128h-576v-256h-64v320z" />
<glyph unicode="&#xe618;" glyph-name="signout" d="M768 320v128h-320v128h320v128l192-192zM704 384v-256h-320v-192l-384 192v832h704v-320h-64v256h-512l256-128v-576h256v192z" />
<glyph unicode="&#xe619;" glyph-name="email" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM256 704h512c9.138 0 18.004-1.962 26.144-5.662l-282.144-329.168-282.144 329.17c8.14 3.696 17.006 5.66 26.144 5.66zM192 256v384c0 1.34 0.056 2.672 0.14 4l187.664-218.942-185.598-185.598c-1.444 5.336-2.206 10.886-2.206 16.54zM768 192h-512c-5.654 0-11.202 0.762-16.54 2.208l182.118 182.118 90.422-105.498 90.424 105.494 182.116-182.12c-5.34-1.44-10.886-2.202-16.54-2.202zM832 256c0-5.654-0.762-11.2-2.206-16.54l-185.6 185.598 187.666 218.942c0.084-1.328 0.14-2.66 0.14-4v-384z" />
<glyph unicode="&#xe61a;" glyph-name="googleplus" d="M437.006 141.838c0-75.068-46.39-134.392-177.758-139.176-76.984 43.786-141.49 106.952-186.908 182.866 23.69 58.496 97.692 103.046 182.316 102.114 24.022-0.252 46.41-4.114 66.744-10.7 55.908-38.866 101-63.152 112.324-107.448 2.114-8.964 3.282-18.206 3.282-27.656zM512 960c-147.94 0-281.196-62.77-374.666-163.098 36.934 20.452 80.538 32.638 126.902 32.638 67.068 0 256.438 0 256.438 0l-57.304-60.14h-67.31c47.496-27.212 72.752-83.248 72.752-145.012 0-56.692-31.416-102.38-75.78-137.058-43.28-33.802-51.492-47.966-51.492-76.734 0-24.542 51.722-61.098 75.5-78.936 82.818-62.112 99.578-101.184 99.578-178.87 0-78.726-68.936-157.104-185.866-183.742 56.348-21.338 117.426-33.048 181.248-33.048 282.77 0 512 229.23 512 512s-229.23 512-512 512zM768 576v-128h-64v128h-128v64h128v128h64v-128h128v-64h-128zM365.768 620.528c11.922-90.776-27.846-149.19-96.934-147.134-69.126 2.082-134.806 65.492-146.74 156.242-11.928 90.788 34.418 160.254 103.53 158.196 69.090-2.074 128.22-76.542 140.144-167.304zM220.886 317.932c-74.68 0-138.128-25.768-182.842-63.864-24.502 59.82-38.044 125.29-38.044 193.932 0 56.766 9.256 111.368 26.312 162.396 7.374-99.442 77.352-176.192 192.97-176.192 8.514 0 16.764 0.442 24.874 1.022-7.95-15.23-13.622-32.19-13.622-49.982 0-29.97 16.488-47.070 36.868-66.894-15.402 0-30.27-0.418-46.516-0.418z" />
<glyph unicode="&#xe61b;" glyph-name="facebook" d="M512 960c282.77 0 512-229.23 512-512 0-261.094-195.438-476.53-448-508.026v380.026h176l16 128h-192v64c0 35.346 28.654 64 64 64h128v128h-128c-106.040 0-192-85.96-192-192v-64h-96v-128h96v-380.026c-252.562 31.496-448 246.932-448 508.026 0 282.77 229.23 512 512 512z" />
<glyph unicode="&#xe61c;" glyph-name="twitter" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM806.242 598.912c0.292-6.508 0.442-13.056 0.442-19.638 0-200.622-152.708-431.964-431.958-431.964-85.736 0-165.536 25.136-232.726 68.214 11.876-1.408 23.962-2.126 36.216-2.126 71.13 0 136.59 24.276 188.55 64.994-66.434 1.22-122.5 45.122-141.824 105.432 9.274-1.768 18.784-2.722 28.566-2.722 13.846 0 27.258 1.856 39.998 5.324-69.452 13.952-121.786 75.312-121.786 148.868 0 0.64 0 1.278 0.016 1.91 20.47-11.37 43.878-18.2 68.764-18.988-40.74 27.222-67.54 73.692-67.54 126.368 0 27.822 7.488 53.904 20.556 76.324 74.878-91.854 186.748-152.292 312.924-158.628-2.588 11.118-3.93 22.702-3.93 34.604 0 83.84 67.98 151.812 151.818 151.812 43.666 0 83.124-18.436 110.818-47.94 34.58 6.808 67.074 19.442 96.412 36.84-11.336-35.454-35.41-65.206-66.752-83.994 30.71 3.668 59.968 11.832 87.194 23.904-20.35-30.448-46.090-57.186-75.758-78.594z" />
<glyph unicode="&#xe61d;" glyph-name="info" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512 32c-229.75 0-416 186.25-416 416s186.25 416 416 416 416-186.25 416-416-186.25-416-416-416zM448 704h128v-128h-128zM640 192h-256v64h64v192h-64v64h192v-256h64z" />
<glyph unicode="&#xe61e;" glyph-name="goline" d="M384 128h640v-128h-640zM384 512h640v-128h-640zM384 896h640v-128h-640zM192 960v-256h-64v192h-64v64zM128 434v-50h128v-64h-192v146l128 60v50h-128v64h192v-146zM256 256v-320h-192v64h128v64h-128v64h128v64h-128v64z" />
<glyph unicode="&#xe61f;" glyph-name="share" d="M864 256c-45.16 0-85.92-18.738-115.012-48.83l-431.004 215.502c1.314 8.252 2.016 16.706 2.016 25.328s-0.702 17.076-2.016 25.326l431.004 215.502c29.092-30.090 69.852-48.828 115.012-48.828 88.366 0 160 71.634 160 160s-71.634 160-160 160-160-71.634-160-160c0-8.622 0.704-17.076 2.016-25.326l-431.004-215.504c-29.092 30.090-69.852 48.83-115.012 48.83-88.366 0-160-71.636-160-160 0-88.368 71.634-160 160-160 45.16 0 85.92 18.738 115.012 48.828l431.004-215.502c-1.312-8.25-2.016-16.704-2.016-25.326 0-88.368 71.634-160 160-160s160 71.632 160 160c0 88.364-71.634 160-160 160z" />
<glyph unicode="&#xe620;" glyph-name="comment" d="M0 343.808l321.344-206.624v149.152l-191.328 118.144 191.328 118.144v150.176l-321.344-207.68v-121.312zM389.312 64h110.592l162.048 768h-111.328l-161.312-768zM702.688 672.8v-150.176l191.264-118.144-191.264-118.112v-149.152l321.312 206.592v121.312l-321.312 207.68z" />
<glyph unicode="&#xe621;" glyph-name="weibo" d="M428.222 493.19c-144.194-6.656-260.742-83.888-260.742-180.224 0-96.202 116.574-168.528 260.742-161.792 144.302 6.576 261.094 96.876 261.094 193.078 0 96.12-116.79 155.594-261.094 148.938zM526.472 250.88c-44.168-56.994-131.53-84.802-216.36-38.858-40.394 21.908-38.884 64.916-38.884 64.916s-16.76 135.842 128.27 152.818c145.192 16.816 171.116-121.856 126.976-178.876zM429.19 341.854c-9.298-6.736-11.182-19.618-6.144-27.62 4.85-8.22 16.142-9.162 25.276-2.318 8.974 7.086 12.45 19.428 7.572 27.62-4.798 7.976-15.952 10.294-26.704 2.318zM360.448 323.154c-27.108-2.802-46.484-26.408-46.484-48.99 0-22.636 21.826-38.264 48.882-35.086 26.974 3.072 48.908 23.928 48.908 46.484 0.054 22.636-20.184 40.582-51.308 37.592zM868.082 960h-712.22c-86.096 0-155.89-69.794-155.89-155.89v-712.22c0-86.096 69.794-155.89 155.89-155.89h712.22c86.096 0 155.89 69.794 155.89 155.89v712.22c0 86.096-69.768 155.89-155.89 155.89zM818.202 280.386c-59.446-126.274-255.462-187.716-400.734-176.344-138.050 10.86-315.526 56.724-333.878 223.798 0 0-9.7 75.668 63.65 173.568 0 0 105.498 147.322 228.378 189.36 122.988 41.85 137.35-28.968 137.35-70.818-6.548-35.49-18.782-56.374 27.38-42.038 0 0 120.886 56.076 170.658 6.332 40.126-40.152 6.63-95.42 6.63-95.42s-16.654-18.404 17.624-25.034c34.358-6.872 142.362-56.914 82.944-183.404zM698.988 629.274c-13.15 0-23.714 10.644-23.714 23.688 0 13.286 10.562 23.956 23.714 23.956 0 0 148.212 27.404 130.48-131.852 0-0.942-0.108-1.698-0.322-2.534-1.672-11.29-11.586-19.94-23.256-19.94-13.204 0-23.956 10.562-23.956 23.74 0-0.026 23.498 106.416-82.944 82.944zM949.518 501.894h-0.216c-3.908-26.948-17.274-29.104-33.198-29.104-19.052 0-34.438 11.964-34.438 31.044 0 16.52 6.846 33.308 6.846 33.308 2.020 6.952 18.136 50.176-10.644 114.796-52.708 88.522-158.856 89.816-171.384 84.776-12.638-4.958-31.286-7.436-31.286-7.436-19.188 0-34.548 15.602-34.548 34.572 0 15.926 10.644 29.4 25.196 33.524 0 0 0.322 0.538 0.808 0.62 1.052 0.216 2.13 1.268 3.26 1.374 14.794 2.83 67.476 13.178 118.702 1.186 91.648-21.396 217.52-110.026 160.904-298.658z" />
<glyph unicode="&#xe623;" glyph-name="book" d="M512 768c0 0-128 128-512 128v-768c388 0 512-128 512-128s124 128 512 128v768c-384 0-512-128-512-128zM128 768c162.688-13.632 262.496-51.264 320-81.76v-515.488c-57.504 30.368-157.312 68-320 81.76v515.488zM896 252.512c-162.752-13.76-262.496-51.328-320-81.76v515.488c57.504 30.496 157.248 68.128 320 81.76v-515.488z" />
<glyph unicode="&#xe624;" glyph-name="git" d="M1004.692 493.606l-447.096 447.080c-25.738 25.754-67.496 25.754-93.268 0l-103.882-103.876 78.17-78.17c12.532 5.996 26.564 9.36 41.384 9.36 53.020 0 96-42.98 96-96 0-14.82-3.364-28.854-9.362-41.386l127.976-127.974c12.532 5.996 26.566 9.36 41.386 9.36 53.020 0 96-42.98 96-96s-42.98-96-96-96-96 42.98-96 96c0 14.82 3.364 28.854 9.362 41.386l-127.976 127.974c-3.042-1.456-6.176-2.742-9.384-3.876v-266.968c37.282-13.182 64-48.718 64-90.516 0-53.020-42.98-96-96-96s-96 42.98-96 96c0 41.796 26.718 77.334 64 90.516v266.968c-37.282 13.18-64 48.72-64 90.516 0 14.82 3.364 28.852 9.36 41.384l-78.17 78.17-295.892-295.876c-25.75-25.776-25.75-67.534 0-93.288l447.12-447.080c25.738-25.75 67.484-25.75 93.268 0l445.006 445.006c25.758 25.762 25.758 67.54-0.002 93.29z" />
<glyph unicode="&#xe900;" glyph-name="qqz" d="M761.562 521.546l-305.314-229.928s122.534-19.085 307.335-16.875l-8.413 38.021 263.313 239.505c4.84 4.404 6.694 11.413 4.708 17.804-1.974 6.394-7.405 10.93-13.783 11.494l-347.126 31.289-135.62 336.932c-2.479 6.184-8.268 10.213-14.657 10.213-6.397 0-12.184-4.029-14.665-10.213l-135.621-336.932-347.128-31.289c-6.374-0.563-11.801-5.1-13.781-11.494-1.987-6.392-0.13-13.401 4.713-17.804l263.31-239.505-78.925-356.295c-1.445-6.521 0.962-13.347 6.142-17.317 5.196-3.9 12.107-4.202 17.608-0.752l298.345 188.885 298.347-188.885c2.554-1.655 5.395-2.401 8.235-2.401 3.299 0 6.583 1.049 9.366 3.146 5.182 3.98 7.593 10.803 6.149 17.325l-62.412 281.762c26.77 14.131 56.254 38.051 56.254 38.051s-116.838-59.919-536.289-30.65l303.994 231.311s-11.952 19.514-392.923 33.442c-25.494 0.938 310.6 66.909 558.84 11.159z" />
<glyph unicode="&#xe9d7;" glyph-name="start" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538zM512 206.502l-223.462-117.48 42.676 248.83-180.786 176.222 249.84 36.304 111.732 226.396 111.736-226.396 249.836-36.304-180.788-176.222 42.678-248.83-223.462 117.48z" />
<glyph unicode="&#xf00a;" glyph-name="github" d="M512 960c-282.75 0-512-229.25-512-512 0-226.25 146.688-418.126 350.156-485.812 25.594-4.688 34.938 11.126 34.938 24.626 0 12.188-0.468 52.562-0.718 95.312-142.376-30.938-172.468 60.376-172.468 60.376-23.312 59.126-56.844 74.876-56.844 74.876-46.532 31.75 3.53 31.126 3.53 31.126 51.406-3.562 78.47-52.75 78.47-52.75 45.688-78.25 119.876-55.626 149-42.5 4.654 33 17.904 55.626 32.5 68.376-113.656 12.938-233.218 56.876-233.218 253.062 0 55.938 19.968 101.562 52.656 137.406-5.218 13-22.844 65.094 5.062 135.562 0 0 42.938 13.75 140.812-52.5 40.812 11.406 84.594 17.032 128.126 17.22 43.5-0.188 87.312-5.876 128.188-17.28 97.688 66.312 140.688 52.5 140.688 52.5 28-70.532 10.376-122.562 5.126-135.5 32.812-35.844 52.626-81.47 52.626-137.406 0-196.688-119.75-240-233.812-252.688 18.438-15.876 34.75-47 34.75-94.75 0-68.438-0.688-123.626-0.688-140.5 0-13.626 9.312-29.562 35.25-24.562 203.312 67.812 349.876 259.688 349.876 485.812 0 282.75-229.25 512-512 512z" />
<glyph unicode="&#xf021;" glyph-name="refresh" d="M863.428 356.572q0-2.856-0.572-4-36.572-153.144-153.144-248.286t-273.142-95.144q-83.43 0-161.43 31.428t-139.144 89.714l-73.714-73.714q-10.856-10.856-25.714-10.856t-25.714 10.856-10.856 25.714v256q0 14.856 10.856 25.714t25.714 10.856h256q14.856 0 25.714-10.856t10.858-25.714-10.856-25.714l-78.286-78.286q40.57-37.714 92-58.286t106.858-20.572q76.572 0 142.856 37.144t106.286 102.286q6.286 9.714 30.286 66.856 4.572 13.144 17.144 13.144h109.714q7.428 0 12.856-5.428t5.428-12.856zM877.714 813.714v-256q0-14.856-10.856-25.714t-25.714-10.856h-256q-14.856 0-25.714 10.856t-10.856 25.714 10.856 25.714l78.856 78.856q-84.572 78.286-199.428 78.286-76.572 0-142.856-37.142t-106.286-102.286q-6.286-9.714-30.286-66.858-4.57-13.144-17.144-13.144h-113.714q-7.43 0-12.856 5.428t-5.43 12.856v4q37.144 153.144 154.286 248.286t274.286 95.142q83.428 0 162.286-31.714t140-89.428l74.286 73.714q10.856 10.856 25.714 10.856t25.714-10.856 10.856-25.714z" />
<glyph unicode="&#xf0c7;" glyph-name="save" d="M219.43 82.286h438.858v219.428h-438.858v-219.428zM731.428 82.286h73.144v512q0 8-5.714 22t-11.428 19.714l-160.572 160.57q-5.714 5.714-19.428 11.43t-22.286 5.714v-237.714q0-22.858-16-38.858t-38.856-16h-329.142q-22.856 0-38.856 16t-16 38.856v237.714h-73.144v-731.43h73.144v237.714q0 22.856 16 38.856t38.856 16h475.43q22.856 0 38.856-16t16-38.856v-237.714zM512 612.572v182.856q0 7.428-5.428 12.858t-12.856 5.428h-109.714q-7.428 0-12.858-5.428t-5.428-12.858v-182.856q0-7.428 5.428-12.856t12.856-5.428h109.714q7.428 0 12.858 5.428t5.428 12.856zM877.714 594.286v-530.286q0-22.856-16-38.856t-38.856-16h-768q-22.856 0-38.856 16t-16 38.856v768q0 22.856 16 38.856t38.856 16h530.286q22.856 0 50.286-11.428t43.428-27.428l160-160q16-16 27.428-43.428t11.428-50.286z" />
<glyph unicode="&#xf0ed;" glyph-name="export" horiz-adv-x="1097" d="M731.429 429.714q0 8-5.143 13.143t-13.143 5.143h-128v201.143q0 7.429-5.429 12.857t-12.857 5.429h-109.714q-7.429 0-12.857-5.429t-5.429-12.857v-201.143h-128q-7.429 0-12.857-5.429t-5.429-12.857q0-8 5.143-13.143l201.143-201.143q5.143-5.143 13.143-5.143t13.143 5.143l200.571 200.571q5.714 6.857 5.714 13.714zM1097.143 301.714q0-90.857-64.286-155.143t-155.143-64.286h-621.714q-105.714 0-180.857 75.143t-75.143 180.857q0 74.286 40 137.143t107.429 94.286q-1.143 17.143-1.143 24.571 0 121.143 85.714 206.857t206.857 85.714q89.143 0 163.143-49.714t107.714-132q40.571 35.429 94.857 35.429 60.571 0 103.429-42.857t42.857-103.429q0-43.429-23.429-78.857 74.286-17.714 122-77.429t47.714-136.286z" />
<glyph unicode="&#xf0ee;" glyph-name="import" horiz-adv-x="1097" d="M731.429 466.286q0 8-5.143 13.143l-201.143 201.143q-5.143 5.143-13.143 5.143t-13.143-5.143l-200.571-200.571q-5.714-6.857-5.714-13.714 0-8 5.143-13.143t13.143-5.143h128v-201.143q0-7.429 5.429-12.857t12.857-5.429h109.714q7.429 0 12.857 5.429t5.429 12.857v201.143h128q7.429 0 12.857 5.429t5.429 12.857zM1097.143 301.714q0-90.857-64.286-155.143t-155.143-64.286h-621.714q-105.714 0-180.857 75.143t-75.143 180.857q0 74.286 40 137.143t107.429 94.286q-1.143 17.143-1.143 24.571 0 121.143 85.714 206.857t206.857 85.714q89.143 0 163.143-49.714t107.714-132q40.571 35.429 94.857 35.429 60.571 0 103.429-42.857t42.857-103.429q0-43.429-23.429-78.857 74.286-17.714 122-77.429t47.714-136.286z" />
<glyph unicode="&#xf11c;" glyph-name="keyboard" horiz-adv-x="1097" d="M219.429 292.571v-54.857q0-9.143-9.143-9.143h-54.857q-9.143 0-9.143 9.143v54.857q0 9.143 9.143 9.143h54.857q9.143 0 9.143-9.143zM292.571 438.857v-54.857q0-9.143-9.143-9.143h-128q-9.143 0-9.143 9.143v54.857q0 9.143 9.143 9.143h128q9.143 0 9.143-9.143zM219.429 585.143v-54.857q0-9.143-9.143-9.143h-54.857q-9.143 0-9.143 9.143v54.857q0 9.143 9.143 9.143h54.857q9.143 0 9.143-9.143zM804.571 292.571v-54.857q0-9.143-9.143-9.143h-493.714q-9.143 0-9.143 9.143v54.857q0 9.143 9.143 9.143h493.714q9.143 0 9.143-9.143zM438.857 438.857v-54.857q0-9.143-9.143-9.143h-54.857q-9.143 0-9.143 9.143v54.857q0 9.143 9.143 9.143h54.857q9.143 0 9.143-9.143zM365.714 585.143v-54.857q0-9.143-9.143-9.143h-54.857q-9.143 0-9.143 9.143v54.857q0 9.143 9.143 9.143h54.857q9.143 0 9.143-9.143zM585.143 438.857v-54.857q0-9.143-9.143-9.143h-54.857q-9.143 0-9.143 9.143v54.857q0 9.143 9.143 9.143h54.857q9.143 0 9.143-9.143zM512 585.143v-54.857q0-9.143-9.143-9.143h-54.857q-9.143 0-9.143 9.143v54.857q0 9.143 9.143 9.143h54.857q9.143 0 9.143-9.143zM731.429 438.857v-54.857q0-9.143-9.143-9.143h-54.857q-9.143 0-9.143 9.143v54.857q0 9.143 9.143 9.143h54.857q9.143 0 9.143-9.143zM950.857 292.571v-54.857q0-9.143-9.143-9.143h-54.857q-9.143 0-9.143 9.143v54.857q0 9.143 9.143 9.143h54.857q9.143 0 9.143-9.143zM658.286 585.143v-54.857q0-9.143-9.143-9.143h-54.857q-9.143 0-9.143 9.143v54.857q0 9.143 9.143 9.143h54.857q9.143 0 9.143-9.143zM804.571 585.143v-54.857q0-9.143-9.143-9.143h-54.857q-9.143 0-9.143 9.143v54.857q0 9.143 9.143 9.143h54.857q9.143 0 9.143-9.143zM950.857 585.143v-201.143q0-9.143-9.143-9.143h-128q-9.143 0-9.143 9.143v54.857q0 9.143 9.143 9.143h64v137.143q0 9.143 9.143 9.143h54.857q9.143 0 9.143-9.143zM1024 155.429v512h-950.857v-512h950.857zM1097.143 667.429v-512q0-30.286-21.429-51.714t-51.714-21.429h-950.857q-30.286 0-51.714 21.429t-21.429 51.714v512q0 30.286 21.429 51.714t51.714 21.429h950.857q30.286 0 51.714-21.429t21.429-51.714z" />
<glyph unicode="&#xf148;" glyph-name="moveup" horiz-adv-x="585" d="M580.891 578.932q-10.269-21.109-33.090-21.109h-109.537v-492.917q0-7.987-5.135-13.122t-13.122-5.135h-401.637q-11.981 0-16.544 10.269-4.564 11.411 2.282 19.968l91.282 109.537q5.135 6.276 14.263 6.276h182.562v365.124h-109.537q-22.82 0-33.090 21.109-9.698 21.109 5.135 38.794l182.562 219.075q10.269 12.551 27.955 12.551t27.955-12.551l182.562-219.075q15.404-18.257 5.135-38.794z" />
<glyph unicode="&#xf149;" glyph-name="movedown" horiz-adv-x="585" d="M18.286 813.714h402.286q7.429 0 12.857-5.429t5.429-13.429v-493.143h109.714q22.857 0 33.143-21.143t-5.143-39.429l-182.857-219.429q-10.286-12.571-28-12.571t-28 12.571l-182.857 219.429q-14.857 17.714-5.143 39.429 10.286 21.143 33.143 21.143h109.714v365.714h-182.857q-8 0-14.286 6.286l-91.429 109.714q-7.429 8-2.286 19.429 5.143 10.857 16.571 10.857z" />
</font></defs></svg> </font></defs></svg>

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
static/css/fonts/icomoon.ttf Executable file → Normal file

Binary file not shown.

BIN
static/css/fonts/icomoon.woff Executable file → Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

99
static/css/login.css Normal file
View File

@ -0,0 +1,99 @@
.wrapper {
margin: 0 auto;
width: 980px;
}
.header .logo {
float: left;
height: 32px;
margin-top: 3px;
}
.header {
margin: 8px 0;
}
.header li {
float: left;
}
.header a {
display: block;
font-weight: bold;
padding: 4px 8px;
color: #333;
line-height: 30px;
text-decoration: none;
}
.header a:hover {
color: #4183C4;
}
.content {
border-top: 1px solid #A4A4A4;
border-bottom: 1px solid #919191;
background-color: #202021;
}
.content h2 {
color: #FFF;
font-size: 70px;
margin: 0 0 60px;
}
.content h3 {
color: #4183c4;
font-size: 21px;
}
.content .form {
width: 320px;
margin-top: 28px;
position: relative;
}
.content .form input {
width: 100%;
background-color: #fafafa;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075) inset;
color: #333;
min-height: 34px;
outline: medium none;
vertical-align: middle;
font-size: 16px;
border: 1px solid #FFF;
padding: 10px;
margin-top: 20px;
}
.content .form input:focus {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075) inset, 0 0 12px rgba(255, 255, 255, 0.75);
}
#msg {
background-color: #fcdede;
border: 1px solid #d2b2b2;
padding: 15px;
font-size: 14px;
color: #911;
position: absolute;
width: 100%;
top: -48px;
}
.footer {
line-height: 30px;
color: #777;
font-size: 12px;
text-align: center;
}
.footer a {
text-decoration: none;
color: #4183c4;
}
.footer a:hover {
text-decoration: underline;
}

View File

@ -1,158 +0,0 @@
/*
* Copyright (c) 2014-present, b3log.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
body {
display: flex;
flex-direction: column;
max-height: 100vh;
}
.main {
flex: 1;
min-height: 1px;
display: flex;
}
.header {
margin: 0;
padding: 5px;
}
.header li {
margin-left: 0;
margin-right: 20px;
}
.header .gravatar {
width: 26px;
border-radius: 13px;
}
.header .logo {
height: 28px;
margin-top: -6px;
}
.header .font-ico {
font-size: 18px;
line-height: 28px;
}
.share-panel {
position: absolute;
z-index: 20;
width: 190px;
padding: 5px 0px;
right: 0px;
line-height: normal;
top: 38px;
border-color: #999;
}
.share-panel .font-ico {
transition: all .2s ease-out 0s;
margin: 0 5px;
width: 24px;
}
.share-panel .font-ico:hover {
transform: rotate(360deg);
}
.footer {
height: 30px;
text-shadow: 0 0 0;
}
#editorDiv {
width: 100%;
height: 70%;
}
.bottom-window-group {
height: 30%;
}
#output {
height: 100%;
width: 100%;
border-width: 0;
margin: 0;
padding: 2px 5px;
border-top: 1px solid #919191;
}
#dialogShare {
margin: 10px 15px 0;
line-height: 28px;
}
#dialogShare a {
white-space: pre-wrap;
word-wrap: break-word;
}
.wrapper {
width: auto;
}
.btn {
padding: 3px 8px;
font-size: 13px;
line-height: 20px;
height: 28px;
}
#editorDivWrap {
width: 70%;
}
#goNews {
width: 30%;
overflow: auto;
border-left: 1px solid #919191;
}
#goNews::-webkit-scrollbar {
display: none;
}
#goNews li a {
display: block;
padding: 8px 10px;
text-shadow: 0 1px 0 #fff;
border-bottom: 1px solid #eee;
color: #666;
}
#goNews li a.fn-right {
padding: 0;
border: 0;
color: #4285f4;
}
#goNews li a:hover {
text-decoration: none;
background-color: #f9f9f9;
}
#goNews li.title {
border-bottom: 1px solid #e5e5e5;
padding: 10px;
font-size: 14px;
line-height: 18px;
background-color: #f9fafb;
}

View File

@ -1,19 +1,3 @@
/*
* Copyright (c) 2014-present, b3log.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#shellOutput, #shellInput { #shellOutput, #shellInput {
width: 100%; width: 100%;
} }

View File

@ -1,89 +1,27 @@
/*
* Copyright (c) 2014-present, b3log.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* themes for side
*
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
* @version 0.1.0.0, Dec 6, 2015
*/
/* start side */ /* start side */
.side { .side {
background-color: #FFF;
width: 20%; width: 20%;
position: absolute; position: absolute;
border-right: 1px solid #9B9B9B;
height: 100%; height: 100%;
z-index: 8; z-index: 8;
flex-flow: column;
display: flex;
} }
.side-max { .side-max {
width: 100%; width: 100%;
z-index: 11; z-index: 11;
} }
/* end side */
/* start side right */ .side .tabs-panel {
.side-right .tabs-panel > div {
overflow: auto; overflow: auto;
} }
/* start side */
.side-right {
flex-flow: column;
}
#outline .ico {
margin: 1px 5px 0 5px;
}
.ico-func {
background-position: -123px -21px;
}
.ico-interface {
background-position: -143px -21px;
}
.ico-const {
background-position: -103px -21px;
}
.ico-var {
background-position: -63px -21px;
}
.ico-struct {
background-position: -83px -21px;
}
.ico-type {
background-position: -163px -21px;
}
.ico-package {
background-position: -183px -21px;
}
/* end side right */
/* start tree */ /* start tree */
.ztree { .ztree {
width: 100%; width: 100%;
padding: 0; padding: 0;
outline: 0px;
border: 0px;
} }
.ztree li a.curSelectedNode { .ztree li a.curSelectedNode {
@ -91,7 +29,6 @@
border-width: 0; border-width: 0;
color: #fff; color: #fff;
height: 18px; height: 18px;
opacity: 1;
} }
.ztree li a:hover { .ztree li a:hover {
@ -99,9 +36,7 @@
} }
.ztree li > a > span.button, .ztree li > a > span.button,
.ztree li > a > span.button.ico-ztree-dir, .ztree li > a > span.button.ico-ztree-dir {
.ztree li > a > span.button.ico-ztree-dir-api,
.ztree li > a > span.button.ico-ztree-dir-workspace {
margin-right: 2px; margin-right: 2px;
} }
@ -114,14 +49,6 @@
background-position: -2px -23px; background-position: -2px -23px;
} }
.ico-ztree-dir-api {
background-position: -22px -23px;
}
.ico-ztree-dir-workspace {
background-position: -42px -23px;
}
.ico-ztree-html { .ico-ztree-html {
background-position: -4px -2px; background-position: -4px -2px;
} }

View File

@ -1,241 +0,0 @@
/*
* Copyright (c) 2014-present, b3log.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
body {
display: flex;
min-height: 100vh;
flex-direction: column;
}
.wrapper {
margin: 0 auto;
width: 980px;
display: flex;
align-items: center;
}
.header .logo {
float: left;
height: 32px;
margin-top: 3px;
}
.header {
margin: 8px 0;
}
.header li {
float: left;
margin-left: 25px;
}
.header a {
display: block;
font-weight: bold;
padding: 5px 0;
color: #333;
line-height: 30px;
text-decoration: none;
}
.header a:hover {
color: #4285f4;
}
.fn-left {
flex: 1;
}
.content {
border-top: 1px solid #A4A4A4;
border-bottom: 1px solid #919191;
background-color: #3b3e43;
flex: 1;
display: flex;
align-items: center;
}
.content h2 {
color: #FFF;
font-size: 70px;
margin: 0 0 60px;
}
.content h3 {
color: #4285f4;
font-size: 21px;
}
.content .form {
padding: 24px 15px;
background-color: #fff;
border-radius: 6px;
width: 320px;
position: relative;
}
.content a {
color: #4285f4;
}
.login__icon {
width: 200px;
transition: all .15s ease-in-out;
padding-right: 24px;
color: #3b3e43;
fill: currentColor;
}
.login__icon:hover {
transform: scale(1.1);
}
.btn {
width: 100%;
color: #fff;
background-color: #2ebc4f;
padding: 10px;
border-radius: 3px;
cursor: pointer;
border: 0;
display: block;
text-align: center;
}
.btn:hover {
text-decoration: none;
background-color: #28a745;
}
.btn:focus {
box-shadow: 0 0 0 0.2em rgba(40, 167, 69, .3);
}
.btn-blue {
background-color: #4285f4;
}
.btn-blue:hover {
background-color: #2a75f3;
}
.btn.btn-white,
.btn.btn-red {
color: #333;
background-color: #fff;
}
.btn.btn-red {
color: #d23f31;
}
.btn.btn-white:hover {
background-color: #ddd;
}
.btn.btn-red:hover {
color: #fff;
background-color: #d23f31;
}
.desc {
color: #6a737d;
font-size: 12px;
}
.more-detail {
display: none;
margin: 0 0 8px 60px;
}
.checkbox {
margin-top: 8px;
display: block;
}
.view-more {
cursor: pointer;
display: block;
text-align: center;
color: #4285f4;
margin-top: 8px;
}
.footer {
line-height: 30px;
color: #777;
font-size: 12px;
text-align: center;
position: relative;
}
.footer a {
text-decoration: none;
color: #4285f4;
}
.footer a:hover {
text-decoration: underline;
}
.footer .github-btns {
height: 25px;
position: absolute;
top: 5px;
right: 0;
}
/* start sign up */
.dir {
color: #4285f4;
font-size: 18px;
word-wrap: break-word;
margin-top: 20px;
}
#dir {
color: #999;
font-size: 13px;
}
.form.sign-up {
margin-top: -108px;
}
#loginBtn,
#signUpBtn {
margin-top: 20px;
font-size: 16px;
}
.start {
text-align: center;
}
.start .btn {
color: #fff;
}
.start svg {
fill: currentColor;
}
.start__aciton {
display: flex;
align-items: center;
}
/* end sign up */

View File

@ -1,25 +1,8 @@
/*
* Copyright (c) 2014-present, b3log.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#startPage { #startPage {
padding: 50px 70px; padding: 50px 70px;
line-height: 28px; line-height: 28px;
white-space: normal; white-space: normal;
word-wrap: break-word; word-wrap: break-word;
overflow: auto;
} }
#startPage a { #startPage a {
@ -32,13 +15,12 @@
} }
#startPage .title { #startPage .title {
background-color: #BBB; background-color: #f5f5f5;
border-bottom-width: 0 !important; border-bottom-width: 0 !important;
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
font-size: 15px; font-size: 15px;
margin-bottom: 10px; margin-bottom: 10px;
padding: 5px 10px; padding: 5px 10px;
color: #FFF;
} }
#startPage .details { #startPage .details {
@ -53,7 +35,7 @@
#startPage .details li.border { #startPage .details li.border {
padding-bottom: 5px; padding-bottom: 5px;
margin-bottom: 5px; margin-bottom: 5px;
border-bottom: 1px solid #919191; border-bottom: 1px solid #f1f1f1;
} }
#startPage .details li.border.workspace { #startPage .details li.border.workspace {
@ -65,22 +47,19 @@
} }
#startPage .news { #startPage .news {
height: 300px;
width: 60%; width: 60%;
float: right; float: right;
border-left: 1px solid #f1f1f1; border-left: 1px solid #f1f1f1;
margin-left: 10%; margin-left: 10%;
padding-left: 10%; padding-left: 10%;
white-space: nowrap;
overflow: hidden;
} }
#startPage .news li { #startPage .news li {
border-bottom: 1px solid #919191; border-bottom: 1px solid #f1f1f1;
} }
#startPage .date { #startPage .date {
color: #bbb; color: #bbb;
font-size: 13px; font-size: 13px;
word-wrap: normal;
white-space: nowrap;
} }

View File

@ -1,179 +0,0 @@
/*
* Copyright (c) 2014-present, b3log.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* themes for dark.
*
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
* @version 0.1.0.0, Dec 6, 2015
*/
.side {
background-color: #303130;
}
.side-right {
background-color: #303130;
color: #999;
}
.footer {
background-color: #181B1D;
border-top: 1px solid #000000;
color: rgba(255, 255, 255, 0.5);
}
.edit-panel {
background-color: #181818;
color: #EBEBEB;
}
.font-ico,
.ico-restore {
color: rgba(255, 255, 255, 0.5);
}
.font-ico:hover,
.ico-restore:hover,
.frame li:hover .font-ico {
color: #e1e1e1;
}
.menu {
background: #252525;
box-shadow: 0 1px 0 0 #353535 inset;
border-bottom: 1px solid #000000;
}
.menu > ul > li > span {
color: #cecece;
text-shadow: #292a2b 0px 1px 0px;
}
.menu > ul > li.selected {
background-color: #494949;
color: #d4d4d4;
box-shadow: 1px 0 0 0 #000000 inset, 1px 0 0 0 #000000, 0 1px 0 0 rgba(255, 255, 255, 0.15) inset;
}
.menu .split {
border-color: #000000;
}
.frame {
color: #f1f1f1;
border: 1px solid #00040a;
box-shadow: 0px 3px 15px 0px rgba(0, 0, 0, 0.65);
background-color: #494949;
text-shadow: 0px 1px 0px #2c2c2c;
}
.frame li:hover,
.zeroclipboard-is-hover {
background-color: #3875D7;
color: #FFF;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
}
.frame li.disabled:hover {
background-color: #494949;
color: #999;
}
.frame .hr {
font-size: 1px;
margin: 2px 3px;
border-top: 1px solid #353535;
border-bottom: 1px solid #565656;
height: 0;
}
.tabs {
box-shadow: 0 -1px 0 0 #000000 inset, 0 1px 0 0 rgba(255, 255, 255, 0.06) inset, 0 1px 0 rgba(255, 255, 255, 0.06);
background-color: #252525;
}
.tabs > div {
background-color: #303030;
color: #adadad;
text-shadow: 0 1px rgba(0, 0, 0, 0.4);
border-right: 1px solid #181B1D;
}
.tabs > div:hover {
background-color: #363636;
}
.tabs > div.current {
color: rgba(255, 255, 255, 0.85);
text-shadow: 0 1px rgba(0, 0, 0, 0.4);
background-color: #555;
}
.dialog-header-bg {
border-bottom: 1px solid #000000;
box-shadow: 0 1px #5b5c5e inset;
background-image: -webkit-linear-gradient(top, #404143 0%, #333537 52%, #252729 52%, #252729 100%);
}
.dialog-title {
font-weight: bold;
color: #ffffff;
}
.dialog-main {
background-color: #DEDEDF;
}
.dialog-footer {
box-shadow: 0 1px 0 0 #353535 inset;
border-top: 1px solid #000000;
background-color: #2B2B2B;
}
.ztree li a {
color: #e1e1e1;
}
.bottom-window-group .tabs-panel {
background-color: #181818;
color: #ebebeb;
}
#dialogPreference .preference {
border: 1px solid #000;
}
/* start layout resize */
.ui-layout-resizer,
.ui-layout-resizer-dragging,
.ui-layout-resizer-open-hover,
.ui-layout-resizer-dragging {
background: #000;
}
.ui-layout-resizer-closed {
background-color: #555555;
}
.ui-layout-resizer-west-closed {
border-right: 1px solid #000;
}
.ui-layout-resizer-south-closed {
border-top: 1px solid #000;
}
.ui-layout-resizer-east-closed {
border-left: 1px solid #000;
}
.ui-layout-resizer-closed-hover { /* hover-color to 'slide open' */
background: #000;
}

View File

@ -1,155 +0,0 @@
/*
* Copyright (c) 2014-present, b3log.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* themes for default.
*
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
* @version 0.1.0.0, Dec 6, 2015
*/
.side {
background-color: #FFF;
}
.side-right {
background-color: #FFF;
}
.footer {
border-top: 1px solid #919191;
background-color: #F0F0F0;
color: #919191;
}
.edit-panel {
background-color: #FFF;
}
.font-ico,
.ico-restore {
color: #666;
}
.font-ico:hover,
.ico-restore:hover,
.frame li:hover .font-ico {
color: #333;
}
.menu {
background: #F0F0F0;
box-shadow: 0 1px 0 0 #E7E7E7 inset;
border-bottom: 1px solid #919191;
}
.menu > ul > li > span {
color: #000;
text-shadow: 0px 1px 0px #efefef;
}
.menu > ul > li.selected {
background-color: #cfcfcf;
box-shadow: 1px 0 0 0 #b6b6b6 inset, 1px 0 0 0 #b6b6b6, 0 1px 0 0 rgba(255, 255, 255, 0.15) inset;
color: #393939;
}
.menu .split {
border-color: #b6b6b6;
}
.frame {
border: 1px solid #5F5F5F;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
background-color: #F8F8F8;
}
.frame li:hover,
.zeroclipboard-is-hover {
background-color: #3875D7;
color: #FFF;
}
.frame li.disabled:hover {
background-color: #F8F8F8;
color: #999;
}
.frame .hr {
font-size: 1px;
margin: 2px 3px;
border-top: 1px solid #e6e6e6;
border-bottom: 1px solid #F8F8F8;
height: 0;
}
.tabs {
box-shadow: 0 -1px 0 0 rgba(6, 6, 6, 0.1) inset, 0 1px 0 0 rgba(255, 255, 255, 0.44999999999999996) inset, 0 1px 0 transparent;
background-color: #E6E6E6;
}
.tabs > div {
background-color: #DDD;
color: #8B8B8B;
border-right: 1px solid #ADADAD;
}
.tabs > div:hover {
background-color: #E4E4E4;
}
.tabs > div.current {
background-color: #bbb;
color: #FFF;
text-shadow: 0 1px rgba(0, 0, 0, 0.4);
}
.dialog-header-bg {
border-bottom: 1px solid #8891A1;
}
.dialog-title {
color: #000;
}
.dialog-main {
background-color: #F0F0F0;
}
#dialogPreference .preference {
border: 1px solid #A4A4A4;
}
/* start layout resize */
.ui-layout-resizer,
.ui-layout-resizer-dragging {
background: #bbb;
}
.ui-layout-resizer-open-hover , /* hover-color to 'resize' */
.ui-layout-resizer-dragging { /* resizer beging 'dragging' */
background: #919191;
}
.ui-layout-resizer-west-closed {
border-right: 1px solid #919191;
}
.ui-layout-resizer-south-closed {
border-top: 1px solid #919191;
}
.ui-layout-resizer-east-closed {
border-left: 1px solid #919191;
}
.ui-layout-resizer-closed-hover { /* hover-color to 'slide open' */
background: #919191;
}

View File

@ -1,28 +1,74 @@
/* /* start icon */
* Copyright (c) 2014-present, b3log.org @font-face {
* font-family: 'icomoon';
* Licensed under the Apache License, Version 2.0 (the "License"); src:url('fonts/icomoon.eot?35cb2z');
* you may not use this file except in compliance with the License. src:url('fonts/icomoon.eot?#iefix35cb2z') format('embedded-opentype'),
* You may obtain a copy of the License at url('fonts/icomoon.woff?35cb2z') format('woff'),
* url('fonts/icomoon.ttf?35cb2z') format('truetype'),
* https://www.apache.org/licenses/LICENSE-2.0 url('fonts/icomoon.svg?35cb2z#icomoon') format('svg');
* font-weight: normal;
* Unless required by applicable law or agreed to in writing, software font-style: normal;
* distributed under the License is distributed on an "AS IS" BASIS, }
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and .font-ico {
* limitations under the License. font-family: 'icomoon';
*/ /* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #666;
cursor: pointer;
font-size: 13px;
line-height: 18px;
}
.font-ico:hover {
color: #333;
}
.ico-play:before {
content: "\e605";
}
.ico-save:before {
content: "\f0c7";
}
.ico-max:before {
content: "\f096";
}
.ico-format:before {
content: "\e60b";
}
.ico-buildrun:before {
content: "\e607";
}
.ico-stop:before {
content: "\e608";
}
.ico-restore:before {
content: "\e60c";
}
.ico-min:before {
content: "\e60d";
position: absolute;
right: 5px;
}
.ico-close:before {
content: "\e60a";
}
/* end ico */
/*
* themes for wide.
*
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
* @version 0.1.0.1, Dec 15, 2015
*/
/* start frame */ /* start frame */
.frame { .frame {
position: absolute; position: absolute;
border: 1px solid #5F5F5F;
background-color: #F8F8F8;
width: 320px; width: 320px;
z-index: 21; z-index: 21;
display: none; display: none;
@ -34,9 +80,17 @@
cursor: pointer; cursor: pointer;
} }
.frame li.disabled, .frame li.disabled {
.frame li.disabled .font-ico, color: #999;
.frame li.disabled:hover .font-ico { }
.frame li:hover {
background-color: #3875D7;
color: #FFF;
}
.frame li.disabled:hover {
background-color: #F8F8F8;
color: #999; color: #999;
} }
@ -50,51 +104,51 @@
color: #FFF; color: #FFF;
} }
.frame .space { .frame .hr {
display: inline-block; background-color: #bdbdbd;
width: 20px; height: 1px;
height: 15px; margin: 0 1px;
}
.frame .font-ico {
margin-right: 5px;
width: 15px;
display: inline-block;
text-align: center;
} }
/* end frame */ /* end frame */
/* start tabs */ /* start tabs */
.tabs { .tabs {
height: 21px; height: 20px;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
background-color: #E6E6E6;
border-top: 1px solid #A4A4A4;
border-bottom: 1px solid #9D9D9D;
} }
.tabs > div { .tabs > div {
float: left; float: left;
line-height: 20px; line-height: 18px;
height: 20px; height: 18px;
padding: 0 5px; padding: 0 5px;
cursor: pointer; cursor: pointer;
background-color: #DDD;
color: #8B8B8B;
border-right: 1px solid #ADADAD;
} }
.tabs > div > span.changed { .tabs > div.current {
font-weight: bold; background-color: #9F9F9F;
} color: #FFF;
.tabs-panel {
overflow: auto;
flex: 1;
height: 100%;
} }
/* end tabs */ /* end tabs */
/* start framework */
.content {
position: relative;
overflow: hidden;
}
/* end framework */
/* start menu */ /* start menu */
.menu { .menu {
display: block !important; background-color: #F0F0F0;
height: 24px;
} }
.menu > ul > li { .menu > ul > li {
@ -102,67 +156,34 @@
} }
.menu > ul > li > span { .menu > ul > li > span {
color: #000;
font-size: 12px; font-size: 12px;
line-height: 21px; line-height: 24px;
padding: 5px;
text-decoration: none;
cursor: pointer; cursor: pointer;
padding: 4px 7px;
}
.menu .split {
float: left;
border-left: 1px solid #919191;
height: 21px;
margin: 0 5px 0 0px
}
.menu img.gravatar {
float: left;
margin: 2px 8px;
height: 17px;
width: 17px;
border-radius: 9px;
}
#buildRun {
color: #6DB14C;
font-size: 19px;
}
#buildRun.ico-stop {
color: #9d0000;
font-size: 16px;
}
.share-panel {
position: absolute;
z-index: 20;
width: 190px;
padding: 5px 0;
right: 0px;
top: 21px;
}
.share-panel .font-ico {
font-size: 20px;
transition:all .2s ease-out 0s;
margin: 0 5px;
width: 24px;
}
.share-panel .font-ico:hover {
transform:rotate(360deg);
} }
/* end menu */ /* end menu */
/* start editor */ /* start editor */
.edit-panel { .edit-panel {
width: 80%;
position: absolute; position: absolute;
left: 20%; left: 20%;
width: 60%; width: 80%;
height: 70%; height: 70%;
overflow: hidden; overflow: hidden;
flex-flow: column; }
display: flex;
.edit-panel .tabs > div {
background-color: #d1d1d1;
border-right-color: #9b9b9b;
color: #333;
cursor: auto;
}
.edit-panel .tabs > div.current {
background-color: #F7F7F7;
} }
.toolbars { .toolbars {
@ -171,7 +192,7 @@
top: 1px; top: 1px;
} }
.ico { .edit-panel .tabs .ico {
background-image: url("../images/ico-file.png"); background-image: url("../images/ico-file.png");
float: left; float: left;
height: 16px; height: 16px;
@ -179,7 +200,7 @@
width: 16px; width: 16px;
} }
/* 统一为 static/js/lib/codemirror-x.x/addon/hint/show-hint.css 中的.CodeMirror-hints */ /* 统一为 static/js/lib/codemirror-4.5/addon/hint/show-hint.css 中的.CodeMirror-hints */
.edit-exprinfo { .edit-exprinfo {
position: absolute; position: absolute;
z-index: 10; z-index: 10;
@ -197,6 +218,7 @@
background: white; background: white;
font-size: 90%; font-size: 90%;
font-family: Consolas, Courier New, monospace;
max-height: 20em; max-height: 20em;
overflow-y: auto; overflow-y: auto;
@ -204,11 +226,7 @@
.CodeMirror, .CodeMirror,
.CodeMirror-hints { .CodeMirror-hints {
font-family: Consolas, 'Courier New', monospace; font-family: Consolas, Courier New, monospace;
}
.CodeMirror-hints .ico {
margin: -1px 2px 0 -1px;
} }
.CodeMirror-focused .cm-matchhighlight { .CodeMirror-focused .cm-matchhighlight {
@ -226,63 +244,58 @@
background: #08f; background: #08f;
color: white; color: white;
} }
.CodeMirror div.CodeMirror-cursor {
border-left: 2px solid #333;
}
.CodeMirror-scrollbar-filler,
.CodeMirror-gutter-filler {
background-color: transparent;
}
/* end editor */ /* end editor */
/* start bottom-window-group */ /* start bottom-window-group */
.bottom-window-group { .bottom-window-group {
width: 80%;
position: absolute;
left: 20%;
width: 80%;
height: 30%;
top: 70%;
z-index: 7;
background-color: #fff; background-color: #fff;
flex-flow: column; }
.bottom-window-group-max {
height: 100%;
left: 0;
top: 0;
width: 100%;
z-index: 11;
}
.bottom-window-group > div > div {
overflow: auto;
} }
.bottom-window-group .output { .bottom-window-group .output {
font-family: Consolas, Courier New, monospace;
padding: 0 5px; padding: 0 5px;
line-height: 16px; line-height: 16px;
font-size: 12px; font-size: 12px;
overflow-x: scroll;
outline: 0;
} }
.bottom-window-group .output pre { .bottom-window-group .output pre {
margin: 0; margin: 0;
font-family: Consolas, 'Courier New', monospace
} }
.bottom-window-group .output .start-build, .bottom-window-group .output .start-build,
.bottom-window-group .output .start-test, .start-vet, .bottom-window-group .output .start-install,
.bottom-window-group .output .start-install { .bottom-window-group .output .start-get {
color: #999; color: #999;
} }
.bottom-window-group .output .build-succ, .bottom-window-group .output .build-succ,
.bottom-window-group .output .test-succ, .vet-succ, .bottom-window-group .output .install-succ,
.bottom-window-group .output .install-succ { .bottom-window-group .output .get-succ {
color: rgb(0,153,0); color: rgb(0,153,0);
} }
.bottom-window-group .output .build-error, .bottom-window-group .output .build-failed,
.bottom-window-group .output .test-error, .vet-error, .bottom-window-group .output .install-failed,
.bottom-window-group .output .install-error { .bottom-window-group .output .get-failed {
color: #9d0000; color: red;
}
.bottom-window-group .output .stderr {
color: gray;
font-style: italic;
}
.bottom-window-group .output .path {
text-decoration: underline;
cursor: pointer;
} }
.bottom-window-group table { .bottom-window-group table {
@ -290,13 +303,8 @@
} }
.bottom-window-group td { .bottom-window-group td {
border-bottom: 1px solid #919191; border-bottom: 1px solid #DDD;
font-size: 12px; line-height: 20px;
line-height: 19px;
}
.bottom-window-group .notification {
outline: 0;
} }
.bottom-window-group .notification .type, .bottom-window-group .notification .type,
@ -305,19 +313,39 @@
padding: 0 5px; padding: 0 5px;
} }
.bottom-window-group .search { .bottom-window-group .search li {
display: flex; cursor: pointer;
flex-flow: column; line-height: 20px;
outline: 0; padding: 0 3px;
word-wrap: normal;
word-break: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bottom-window-group .search li.selected {
background-color: #3875d7;
color: #FFF;
}
.bottom-window-group .search .path {
color: #999;
font-size: 12px;
}
.bottom-window-group .search li.selected .path {
color: #FFF;
} }
/* end bottom-window-group */ /* end bottom-window-group */
/* start footer */ /* start footer */
.footer { .footer {
box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.06) inset; border-top: 1px solid #919191;
background-color: #F0F0F0;
padding-left: 5px; padding-left: 5px;
height: 19px;
line-height: 18px; line-height: 18px;
display: block !important;
} }
.footer .cursor { .footer .cursor {
@ -328,7 +356,7 @@
float: right; float: right;
display: none; display: none;
cursor: pointer; cursor: pointer;
background-color: #9d0000; background-color: red;
color: #FFF; color: #FFF;
margin: 1px 5px; margin: 1px 5px;
padding: 0 2px; padding: 0 2px;

View File

@ -1,6 +0,0 @@
.dialog-background{height:100%;left:0;opacity:.3;position:absolute;top:0;width:100%;display:none;background-color:#000;z-index:99}.dialog-panel{position:absolute;z-index:100;display:none;-moz-user-select:none;user-select:none;box-shadow:0 2px 10px 1px #000}.dialog-title{float:left;line-height:22px;margin-left:3px;font-weight:700}.dialog-header-bg{height:23px;background-color:#bbb;cursor:move;width:100%}.dialog-close-icon{float:right;margin:3px;text-decoration:none}.dialog-close-icon:hover{text-decoration:none}.dialog-main>div{width:100%}.dialog-footer{padding:10px;text-align:right}#dialogCloseEditor button,.dialog-footer button{margin:0 5px}#dialogAlert,#dialogRemoveConfirm,.dialog-form,.dialog-prompt{padding:10px 15px 0;overflow:hidden}.dialog-main input,.dialog-main select{width:100%;margin:2px auto}#dialogGoFilePrompt>ul{position:relative;height:260px;overflow:auto;margin-top:5px;background-color:#fff;border:1px solid #919191}#dialogPreference{margin:10px}#dialogPreference .tabs-panel{padding:10px}#dialogPreference .preference{margin-bottom:10px}#dialogPreference img.gravatar{width:48px;height:48px}
::-webkit-scrollbar{background:0 0;width:16px;height:16px}::-webkit-scrollbar-corner{display:none;background-color:transparent}::-webkit-scrollbar-thumb{border:solid 0 transparent;border-right-width:4px;border-left-width:4px;border-radius:9px;box-shadow:inset 0 0 0 1px rgba(128,128,128,.2),inset 0 0 0 4px rgba(128,128,128,.2)}::-webkit-scrollbar-thumb:horizontal{border-bottom-width:4px;border-top-width:4px}body{font-size:13px;margin:0;color:#000;overflow:hidden;font-family:Helvetica}ul{padding:0;margin:0;list-style:none}*{box-sizing:border-box}a{color:#4183c4;text-decoration:none}a:hover{text-decoration:underline}img{vertical-align:middle}button,input{font-family:Helvetica}.fn-left{float:left}.fn-right{float:right}.fn-clear:after,.fn-clear:before{display:table;content:""}.fn-clear:after{clear:both}.fn-none{display:none}.ft-small{color:#999;font-size:12px}.ft-red{color:#9d0000}.list li{cursor:pointer;line-height:20px;padding:0 3px;word-wrap:normal;word-break:normal;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.list li.selected,.list li:hover{background-color:#3875d7;color:#fff}.list li.selected .ft-small,.list li:hover .ft-small{color:#fff}@font-face{font-family:icomoon;src:url(fonts/icomoon.eot?lqk80d);src:url(fonts/icomoon.eot?lqk80d#iefix) format('embedded-opentype'),url(fonts/icomoon.ttf?lqk80d) format('truetype'),url(fonts/icomoon.woff?lqk80d) format('woff'),url(fonts/icomoon.svg?lqk80d#icomoon) format('svg');font-weight:400;font-style:normal}[class*=" ico-"],[class^=ico-]{font-family:icomoon!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;cursor:pointer;font-size:13px;line-height:20px}.ico-qqz:before{content:"\e900"}.ico-find:before{content:"\e602"}.ico-findfiles:before{content:"\e603"}.ico-editor:before{content:"\e604"}.ico-notification:before{content:"\e607"}.ico-price:before{content:"\e616"}.ico-report:before{content:"\e605"}.ico-git:before{content:"\e624"}.ico-book:before{content:"\e623"}.ico-start:before{content:"\e9d7";text-shadow:0 0 rgba(0,0,0,.4)}.ico-tree:before{content:"\e600"}.ico-build:before{content:"\e601"}.ico-export:before{content:"\f0ed"}.ico-import:before{content:"\f0ee"}.ico-keyboard:before{content:"\f11c"}.ico-moveup:before{content:"\f148"}.ico-movedown:before{content:"\f149"}.ico-weibo:before{content:"\e621"}.ico-uniE608:before{content:"\e608"}.ico-max:before{content:"\e609"}.ico-remove:before{content:"\e60b"}.ico-buildrun:before{content:"\e60c"}.ico-about:before{content:"\e60d"}.ico-undo:before{content:"\e60e"}.ico-stop:before{content:"\e60f"}.ico-close:before{content:"\e611";text-shadow:0 0 rgba(0,0,0,.4)}.ico-format:before{content:"\e612"}.ico-restore:before{content:"\e613"}.toolbars .ico-restore:before{content:"\e60a"}.ico-min:before{content:"\e614";position:absolute;right:5px}.ico-redo:before{content:"\e615"}.ico-uniE617:before{content:"\e617"}.ico-signout:before{content:"\e618"}.ico-email:before{content:"\e619"}.ico-googleplus:before{content:"\e61a"}.ico-facebook:before{content:"\e61b"}.ico-twitter:before{content:"\e61c"}.ico-info:before{content:"\e61d"}.ico-goline:before{content:"\e61e"}.ico-share:before{content:"\e61f"}.ico-comment:before{content:"\e620"}.ico-github:before{content:"\f00a"}.ico-refresh:before{content:"\f021"}.ico-save:before{content:"\f0c7"}
.frame{position:absolute;width:320px;z-index:21;display:none}.frame li{padding:0 5px;line-height:25px;cursor:pointer}.frame li.disabled,.frame li.disabled .font-ico,.frame li.disabled:hover .font-ico{color:#999}.frame a{color:#000;text-decoration:none}.frame a:hover,.frame li:hover a{color:#fff}.frame .space{display:inline-block;width:20px;height:15px}.frame .font-ico{margin-right:5px;width:15px;display:inline-block;text-align:center}.tabs{height:21px;overflow:hidden;width:100%}.tabs>div{float:left;line-height:20px;height:20px;padding:0 5px;cursor:pointer}.tabs>div>span.changed{font-weight:700}.tabs-panel{overflow:auto;flex:1;height:100%}.menu{display:block!important}.menu>ul>li{float:left}.menu>ul>li>span{font-size:12px;line-height:21px;cursor:pointer;padding:4px 7px}.menu .split{float:left;border-left:1px solid #919191;height:21px;margin:0 5px 0 0}.menu img.gravatar{float:left;margin:2px 8px;height:17px;width:17px;border-radius:9px}#buildRun{color:#6db14c;font-size:19px}#buildRun.ico-stop{color:#9d0000;font-size:16px}.share-panel{position:absolute;z-index:20;width:190px;padding:5px 0;right:0;top:21px}.share-panel .font-ico{font-size:20px;transition:all .2s ease-out 0s;margin:0 5px;width:24px}.share-panel .font-ico:hover{transform:rotate(360deg)}.edit-panel{position:absolute;left:20%;width:60%;height:70%;overflow:hidden;flex-flow:column;display:flex}.toolbars{position:absolute;right:5px;top:1px}.ico{background-image:url(../images/ico-file.png);float:left;height:16px;margin:2px 0 0 -2px;width:16px}.edit-exprinfo{position:absolute;z-index:10;overflow:hidden;list-style:none;margin:0;padding:2px;-webkit-box-shadow:2px 3px 5px rgba(0,0,0,.2);-moz-box-shadow:2px 3px 5px rgba(0,0,0,.2);box-shadow:2px 3px 5px rgba(0,0,0,.2);border-radius:3px;border:1px solid silver;background:#fff;font-size:90%;max-height:20em;overflow-y:auto}.CodeMirror,.CodeMirror-hints{font-family:Consolas,'Courier New',monospace}.CodeMirror-hints .ico{margin:-1px 2px 0 -1px}.CodeMirror-focused .cm-matchhighlight{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);background-position:bottom;background-repeat:repeat-x}.CodeMirror-hint{padding-right:18px;max-width:none}.CodeMirror-hint:hover{background:#08f;color:#fff}.CodeMirror div.CodeMirror-cursor{border-left:2px solid #333}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:transparent}.bottom-window-group{background-color:#fff;flex-flow:column}.bottom-window-group .output{font-family:Consolas,Courier New,monospace;padding:0 5px;line-height:16px;font-size:12px;overflow-x:scroll;outline:0}.bottom-window-group .output pre{margin:0;font-family:Consolas,'Courier New',monospace}.bottom-window-group .output .start-build,.bottom-window-group .output .start-install,.bottom-window-group .output .start-test,.start-vet{color:#999}.bottom-window-group .output .build-succ,.bottom-window-group .output .install-succ,.bottom-window-group .output .test-succ,.vet-succ{color:#090}.bottom-window-group .output .build-error,.bottom-window-group .output .install-error,.bottom-window-group .output .test-error,.vet-error{color:#9d0000}.bottom-window-group .output .stderr{color:gray;font-style:italic}.bottom-window-group .output .path{text-decoration:underline;cursor:pointer}.bottom-window-group table{width:100%}.bottom-window-group td{border-bottom:1px solid #919191;font-size:12px;line-height:19px}.bottom-window-group .notification{outline:0}.bottom-window-group .notification .severity,.bottom-window-group .notification .type{width:50px;padding:0 5px}.bottom-window-group .search{display:flex;flex-flow:column;outline:0}.footer{box-shadow:0 1px 0 0 rgba(255,255,255,.06) inset;padding-left:5px;line-height:18px;display:block!important}.footer .cursor{cursor:pointer}.notification-count{float:right;display:none;cursor:pointer;background-color:#9d0000;color:#fff;margin:1px 5px;padding:0 2px;border-radius:3px;line-height:16px}
.side{width:20%;position:absolute;height:100%;z-index:8;flex-flow:column;display:flex}.side-max{width:100%;z-index:11}.side-right .tabs-panel>div{overflow:auto}.side-right{flex-flow:column}#outline .ico{margin:1px 5px 0 5px}.ico-func{background-position:-123px -21px}.ico-interface{background-position:-143px -21px}.ico-const{background-position:-103px -21px}.ico-var{background-position:-63px -21px}.ico-struct{background-position:-83px -21px}.ico-type{background-position:-163px -21px}.ico-package{background-position:-183px -21px}.ztree{width:100%;padding:0;outline:0;border:0}.ztree li a.curSelectedNode{background-color:#3875d7;border-width:0;color:#fff;height:18px;opacity:1}.ztree li a:hover{text-decoration:none}.ztree li>a>span.button,.ztree li>a>span.button.ico-ztree-dir,.ztree li>a>span.button.ico-ztree-dir-api,.ztree li>a>span.button.ico-ztree-dir-workspace{margin-right:2px}.ztree li>a>span.button{background-image:url(../images/ico-file.png);margin-right:0}.ico-ztree-dir{background-position:-2px -23px}.ico-ztree-dir-api{background-position:-22px -23px}.ico-ztree-dir-workspace{background-position:-42px -23px}.ico-ztree-html{background-position:-4px -2px}.ico-ztree-go{background-position:-22px -2px}.ico-ztree-css{background-position:-42px -2px}.ico-ztree-img{background-position:-63px -2px}.ico-ztree-other{background-position:-83px -2px}.ico-ztree-text{background-position:-103px -2px}.ico-ztree-sql{background-position:-123px -2px}.ico-ztree-pro{background-position:-142px -2px}.ico-ztree-md{background-position:-162px -2px}.ico-ztree-js{background-position:-182px -2px}.ico-ztree-xml{background-position:-202px -2px}
#startPage{padding:50px 70px;line-height:28px;white-space:normal;word-wrap:break-word;overflow:auto}#startPage a{color:#4183c4;text-decoration:none}#startPage a:hover{text-decoration:underline}#startPage .title{background-color:#bbb;border-bottom-width:0!important;border-radius:3px 3px 0 0;font-size:15px;margin-bottom:10px;padding:5px 10px;color:#fff}#startPage .details{width:30%;float:left}#startPage .details label{color:#666}#startPage .details li.border{padding-bottom:5px;margin-bottom:5px;border-bottom:1px solid #919191}#startPage .details li.border.workspace{line-height:18px;padding-bottom:10px!important;word-wrap:break-word;white-space:normal;word-break:break-all}#startPage .news{width:60%;float:right;border-left:1px solid #f1f1f1;margin-left:10%;padding-left:10%;white-space:nowrap;overflow:hidden}#startPage .news li{border-bottom:1px solid #919191}#startPage .date{color:#bbb;font-size:13px;word-wrap:normal;white-space:nowrap}
#dialogAboutDialog .dialog-main{background-color:#fff}#dialogAbout{margin:35px 20px;line-height:28px}#dialogAbout .item{margin:0 10px}#dialogAbout a{color:#4183c4;text-decoration:none}#dialogAbout a:hover{text-decoration:underline}#dialogAbout label{color:#666}#dialogAbout img{width:100px;float:left;margin-right:60px}#dialogAbout .space{margin-bottom:6px;border-bottom:1px solid #919191;padding-bottom:6px}#dialogAbout .thx ul{margin-left:50px}#dialogAbout .thx a{width:80px;display:inline-block}#dialogAbout .license{color:#999;font-size:12px;line-height:normal;height:85px;overflow-x:hidden;word-wrap:break-word}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,95 +0,0 @@
/*
* Copyright (c) 2014-present, b3log.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* @file bottomGroup.js
*
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
* @author <a href="http://88250.b3log.org">Liang Ding</a>
* @version 1.1.1.1, Mar 15, 2017
*/
var bottomGroup = {
tabs: undefined,
searchTab: undefined,
init: function () {
this._initTabs();
this._initFrame();
$('.bottom-window-group .output').click(function () {
$(this).focus();
});
$('.bottom-window-group .output').on('click', '.path', function (event) {
var $path = $(this),
tId = tree.getTIdByPath($path.data("path"));
tree.openFile(tree.fileTree.getNodeByTId(tId),
CodeMirror.Pos($path.data("line") - 1, $path.data("column") - 1));
event.preventDefault();
return false;
});
},
_initFrame: function () {
$(".bottom-window-group .output").parent().mouseup(function (event) {
event.stopPropagation();
if (event.button === 0) { // 左键
$(".bottom-window-group .frame").hide();
return;
}
// event.button === 2 右键
var left = event.screenX,
$it = $(this);
if ($(".side").css("left") === "auto" || $(".side").css("left") === "0px") {
left = event.screenX - $(".side").width();
}
$(".bottom-window-group .frame").show().css({
"left": left + "px",
"top": (event.offsetY + event.target.offsetTop - $it.scrollTop() - 10) + "px"
});
return;
});
},
clear: function (id) {
$('.bottom-window-group .' + id + ' > div').text('');
},
resetOutput: function () {
this.clear('output');
bottomGroup.tabs.setCurrent("output");
windows.flowBottom();
},
_initTabs: function () {
this.tabs = new Tabs({
id: ".bottom-window-group",
clickAfter: function (id) {
this._$tabsPanel.find("." + id).focus();
}
});
},
fillOutput: function (data) {
var $output = $('.bottom-window-group .output');
data = data.replace(/\r/g, '');
data = data.replace(/\n/g, '<br/>');
if (-1 !== data.indexOf("<br/>")) {
data = Autolinker.link(data);
}
$output.find("div").html(data);
$output.parent().scrollTop($output[0].scrollHeight);
}
};

View File

@ -1,11 +1,11 @@
/* /*
* Copyright (c) 2014-present, b3log.org * Copyright (C) 2011, Liyuan Li
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* https://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -13,18 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
/*
* @file dialog.js
*
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
* @version 1.0.0.1, Dec 8, 2015
*/
(function ($) { (function ($) {
$.fn.extend({ $.fn.extend({
dialog: { dialog: {
version: "0.0.1.7", version: "0.0.1.7",
author: "v@b3log.org" author: "lly219@gmail.com"
} }
}); });
@ -44,7 +37,7 @@
"closeIconHover": "dialog-close-icon-hover", "closeIconHover": "dialog-close-icon-hover",
"title": "dialog-title" "title": "dialog-title"
} }
}; }
}; };
$.extend(Dialog.prototype, { $.extend(Dialog.prototype, {
@ -141,6 +134,22 @@
$($("#" + id + "Dialog ." + styleClass.main + " div").get(0)).append(cloneObj); $($("#" + id + "Dialog ." + styleClass.main + " div").get(0)).append(cloneObj);
$(cloneObj).show(); $(cloneObj).show();
// Sets position.
var top = "", left = "",
$dialog = $("#" + id + "Dialog");
if (settings.position) {
top = settings.position.top;
left = settings.position.left;
} else {
// 20(footer) + 23(header)
top = parseInt((windowH - dialogH - 43) / 2);
left = parseInt((windowW - dialogW) / 2);
}
$dialog.css({
"top": top + "px",
"left": left + "px"
});
// Bind event. // Bind event.
$("#" + id + "Dialog ." + styleClass.closeIcon).bind("click", function () { $("#" + id + "Dialog ." + styleClass.closeIcon).bind("click", function () {
$.dialog._close(id, settings); $.dialog._close(id, settings);
@ -165,15 +174,6 @@
$.dialog._close(id, settings); $.dialog._close(id, settings);
} }
}); });
$(window).resize(function () {
var height = $("body").height() > $(window).height() ? $("body").height() : $(window).height();
$(".dialog-background").height(height);
});
if (typeof settings.afterInit === "function") {
settings.afterInit();
}
}, },
_bindMove: function (id, className) { _bindMove: function (id, className) {
$("#" + id + "Dialog ." + className).mousedown(function (event) { $("#" + id + "Dialog ." + className).mousedown(function (event) {
@ -206,12 +206,12 @@
if (positionX > $(window).width() - $(dialog).width()) { if (positionX > $(window).width() - $(dialog).width()) {
positionX = $(window).width() - $(dialog).width(); positionX = $(window).width() - $(dialog).width();
} }
if (positionY > $(window).height() - $(dialog).height()) {
positionY = $(window).height() - $(dialog).height();
}
if (positionY < 0) { if (positionY < 0) {
positionY = 0; positionY = 0;
} }
if (positionY > $(window).height() - $(dialog).height()) {
positionY = $(window).height() - $(dialog).height();
}
dialog.style.left = positionX + "px"; dialog.style.left = positionX + "px";
dialog.style.top = positionY + "px"; dialog.style.top = positionY + "px";
}; };
@ -227,7 +227,7 @@
_document.ondragstart = null; _document.ondragstart = null;
_document.onselectstart = null; _document.onselectstart = null;
_document.onselect = null; _document.onselect = null;
}; }
}); });
}, },
_close: function (id, settings) { _close: function (id, settings) {
@ -251,41 +251,20 @@
_openDialog: function (target, msg) { _openDialog: function (target, msg) {
var inst = this._getInst(target); var inst = this._getInst(target);
var id = inst.id, var id = inst.id,
settings = inst.settings, settings = inst.settings;
top = "", left = "",
$dialog = $("#" + id + "Dialog"),
windowH = $(window).height(),
windowW = $(window).width(),
dialogH = settings.height ? settings.height : parseInt(windowH * 0.6),
dialogW = settings.width ? settings.width : parseInt(windowW * 0.6);
// Sets position. $("#" + id + "Dialog").show();
if (settings.position) {
top = settings.position.top;
left = settings.position.left;
} else {
// 20(footer) + 23(header)
top = parseInt((windowH - dialogH - 43) / 2);
if (top < 0) {
top = 0;
}
left = parseInt((windowW - dialogW) / 2);
}
$dialog.css({
"top": top + "px",
"left": left + "px"
}).show();
if (settings.modal) { if (settings.modal) {
var styleClass = this._getDefaults($.dialog._defaults, settings, "styleClass"); var styleClass = this._getDefaults($.dialog._defaults, settings, "styleClass");
$("." + styleClass.background).show(); $("." + styleClass.background).show();
} }
$("#" + id + "Dialog .dialog-footer button:eq(0)").focus();
if (typeof settings.afterOpen === "function") { if (typeof settings.afterOpen === "function") {
settings.afterOpen(msg); settings.afterOpen(msg);
} }
$("#" + id + "Dialog .dialog-footer button:eq(0)").focus();
}, },
_updateDialog: function (target, data) { _updateDialog: function (target, data) {
var inst = this._getInst(target); var inst = this._getInst(target);

View File

@ -1,224 +1,84 @@
/*
* Copyright (c) 2014-present, b3log.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* @file editor.js
*
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
* @author <a href="http://88250.b3log.org">Liang Ding</a>
* @version 1.1.1.0, Jan 12, 2016
*/
var editors = { var editors = {
autocompleteMutex: false,
data: [], data: [],
tabs: {}, tabs: {},
getEditorByPath: function (path) {
for (var i = 0, ii = editors.data.length; i < ii; i++) {
if (editors.data[i].editor.options.path === path) {
return editors.data[i].editor;
}
}
},
close: function () {
$('.edit-panel .tabs > div[data-index="' + $('.edit-panel .frame').data('index') + ']').find('.ico-close').click();
},
closeOther: function () {
var currentIndex = $(".edit-panel .frame").data("index");
// 设置全部关闭标识
var removeData = [];
$(".edit-panel .tabs > div").each(function (i) {
if (currentIndex !== $(this).data("index")) {
removeData.push($(this).data("index"));
}
});
if (removeData.length === 0) {
return false;
}
var firstIndex = removeData.splice(0, 1);
$("#dialogCloseEditor").data("removeData", removeData);
// 开始关闭
$('.edit-panel .tabs > div[data-index="' + firstIndex + '"]').find(".ico-close").click();
},
_removeAllMarker: function () {
var removeData = $("#dialogCloseEditor").data("removeData");
if (removeData && removeData.length > 0) {
var removeIndex = removeData.splice(0, 1);
$("#dialogCloseEditor").data("removeData", removeData);
$('.edit-panel .tabs > div[data-index="' + removeIndex + '"] .ico-close').click();
}
if (wide.curEditor) {
wide.curEditor.focus();
}
},
_initClose: function () {
new ZeroClipboard($("#copyFilePath"));
// 关闭、关闭其他、关闭所有
$(".edit-panel").on("mouseup", '.tabs > div', function (event) {
event.stopPropagation();
if (event.button === 0) { // 左键
$(".edit-panel .frame").hide();
return false;
}
// event.button === 2 右键
var left = event.screenX;
if ($(".side").css("left") === "auto" || $(".side").css("left") === "0px") {
left = event.screenX - $(".side").width();
}
$(".edit-panel .frame").show().css({
"left": left + "px",
"top": "21px"
}).data('index', $(this).data("index"));
$("#copyFilePath").attr('data-clipboard-text', $(this).find("span:eq(0)").attr("title"));
return false;
});
},
init: function () { init: function () {
$("#dialogCloseEditor").dialog({
"modal": true,
"height": 90,
"width": 260,
"title": config.label.tip,
"hideFooter": true,
"afterOpen": function (fileName) {
$("#dialogCloseEditor > div:eq(0)").html(config.label.file
+ ' <b>' + fileName + '</b>. ' + config.label.confirm_save + '?');
$("#dialogCloseEditor button:eq(0)").focus();
},
"afterInit": function () {
$("#dialogCloseEditor button.save").click(function () {
var i = $("#dialogCloseEditor").data("index");
wide.fmt(editors.data[i].id, editors.data[i].editor);
editors.tabs.del(editors.data[i].id);
$("#dialogCloseEditor").dialog("close");
editors._removeAllMarker();
});
$("#dialogCloseEditor button.discard").click(function () {
var i = $("#dialogCloseEditor").data("index");
editors.tabs.del(editors.data[i].id);
$("#dialogCloseEditor").dialog("close");
editors._removeAllMarker();
});
$("#dialogCloseEditor button.cancel").click(function (event) {
$("#dialogCloseEditor").dialog("close");
editors._removeAllMarker();
});
}
});
editors.tabs = new Tabs({ editors.tabs = new Tabs({
id: ".edit-panel", id: ".edit-panel",
setAfter: function () {
if (wide.curEditor) {
wide.curEditor.focus();
}
},
clickAfter: function (id) { clickAfter: function (id) {
if (id === 'startPage') { if (id === 'startPage') {
wide.curEditor = undefined;
$(".footer .cursor").text('');
wide.refreshOutline();
return false; return false;
} }
},
removeBefore: function (id) { // set tree node selected
if (id === 'startPage') { // 当前关闭的 tab 是起始页 var node = tree.fileTree.getNodeByTId(id);
editors._removeAllMarker(); tree.fileTree.selectNode(node);
return true; wide.curNode = node;
}
for (var i = 0, ii = editors.data.length; i < ii; i++) { for (var i = 0, ii = editors.data.length; i < ii; i++) {
if (editors.data[i].id === id) { if (editors.data[i].id === id) {
if (editors.data[i].editor.doc.isClean()) { wide.curEditor = editors.data[i].editor;
editors._removeAllMarker();
return true;
} else {
$("#dialogCloseEditor").dialog("open", $('.edit-panel .tabs > div[data-index="'
+ editors.data[i].id + '"] > span:eq(0)').text());
$("#dialogCloseEditor").data("index", i);
return false;
}
break; break;
} }
} }
wide.curEditor.focus();
}, },
removeAfter: function (id, nextId) { removeAfter: function (id, nextId) {
if ($(".edit-panel .tabs > div").length === 0) { if (id === 'startPage') {
// 全部 tab 都关闭时才 disables 菜单中“全部关闭”的按钮 return false;
menu.disabled(['close-all']);
} }
// 移除编辑器
for (var i = 0, ii = editors.data.length; i < ii; i++) { for (var i = 0, ii = editors.data.length; i < ii; i++) {
if (editors.data[i].id === id) { if (editors.data[i].id === id) {
wide.fmt(tree.fileTree.getNodeByTId(editors.data[i].id).path, editors.data[i].editor);
editors.data.splice(i, 1); editors.data.splice(i, 1);
break; break;
} }
} }
if (editors.data.length === 0) { // 起始页可能存在,所以用编辑器数据判断
menu.disabled(['save-all', 'build', 'run', 'go-test', 'go-vet', 'go-mod', 'go-install',
'find', 'find-next', 'find-previous', 'replace', 'replace-all',
'format', 'autocomplete', 'jump-to-decl', 'expr-info', 'find-usages', 'toggle-comment',
'edit']);
// remove selected tree node
tree.fileTree.cancelSelectedNode();
wide.curNode = undefined;
wide.curEditor = undefined;
wide.refreshOutline();
$(".footer .cursor").text('');
return false;
}
if (!nextId) { if (!nextId) {
// 编辑器区域不存在打开的 Tab // 不存在打开的编辑器
// remove selected tree node // remove selected tree node
tree.fileTree.cancelSelectedNode(); tree.fileTree.cancelSelectedNode();
wide.curNode = undefined; wide.curNode = undefined;
wide.curEditor = undefined; wide.curEditor = undefined;
wide.refreshOutline();
$(".footer .cursor").text(''); menu.disabled(['save-all', 'close-all', 'run', 'go-get', 'go-install']);
$(".toolbars").hide();
return false; return false;
} }
if (nextId === editors.tabs.getCurrentId()) { if (nextId === editors.tabs.getCurrentId()) {
// 关闭的不是当前编辑器
return false; return false;
} }
// set tree node selected
var node = tree.fileTree.getNodeByTId(nextId);
tree.fileTree.selectNode(node);
wide.curNode = node;
for (var i = 0, ii = editors.data.length; i < ii; i++) {
if (editors.data[i].id === nextId) {
wide.curEditor = editors.data[i].editor;
break;
}
}
}
});
$(".edit-panel .tabs").on("dblclick", function () {
if ($(".toolbars .ico-max").length === 1) {
windows.maxEditor();
} else {
windows.restoreEditor();
} }
}); });
this._initCodeMirrorHotKeys(); this._initCodeMirrorHotKeys();
this.openStartPage(); this.openStartPage()
this._initClose();
}, },
openStartPage: function () { openStartPage: function () {
wide.curEditor = undefined;
wide.refreshOutline();
$(".footer .cursor").text('');
var dateFormat = function (time, fmt) { var dateFormat = function (time, fmt) {
var date = new Date(time); var date = new Date(time);
var dateObj = { var dateObj = {
@ -242,13 +102,12 @@ var editors = {
editors.tabs.add({ editors.tabs.add({
id: "startPage", id: "startPage",
title: '<span title="' + config.label.start_page title: '<span title="' + config.label.start_page + '">' + config.label.start_page + '</span>',
+ '"><span class="ico-start font-ico"></span> ' + config.label.start_page + '</span>',
content: '<div id="startPage"></div>', content: '<div id="startPage"></div>',
after: function () { after: function () {
$("#startPage").load('/start?sid=' + config.wideSessionId); $("#startPage").load('/start');
$.ajax({ $.ajax({
url: "https://ld246.com/apis/articles?tags=wide,golang&p=1&size=20", url: "http://symphony.b3log.org/apis/articles?tags=wide,golang&p=1&size=30",
type: "GET", type: "GET",
dataType: "jsonp", dataType: "jsonp",
jsonp: "callback", jsonp: "callback",
@ -258,22 +117,21 @@ var editors = {
return; return;
} }
// 按 size = 20 取,但只保留最多 9 // 按 size = 30 取,但只保留最多 10
var length = articles.length; var length = articles.length;
if (length > 9) { if (length > 10) {
length = 9; length = 10;
} }
var listHTML = "<ul><li class='title'>" + config.label.community + var listHTML = "<ul><li class='title'>" + config.label.community + "</li>";
"<a href='https://ld246.com/article/1437497122181' target='_blank' class='fn-right'>边看边练</li>";
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
var article = articles[i]; var article = articles[i];
listHTML += "<li>" listHTML += "<li>"
+ "<a target='_blank' href='" + "<a target='_blank' href='http://symphony.b3log.org"
+ article.articlePermalink + "'>" + article.articlePermalink + "'>"
+ article.articleTitle + "</a>&nbsp; <span class='date'>" + article.articleTitle + "</a>&nbsp; <span class='date'>"
+ dateFormat(article.articleCreateTime, 'yyyy-MM-dd'); + dateFormat(article.articleCreateTime, 'yyyy-MM-dd hh:mm');
+"</span></li>"; +"</span></li>"
} }
$("#startPage .news").html(listHTML + "</ul>"); $("#startPage .news").html(listHTML + "</ul>");
@ -283,12 +141,11 @@ var editors = {
}); });
}, },
getCurrentId: function () { getCurrentId: function () {
var ret = editors.tabs.getCurrentId(); var currentId = editors.tabs.getCurrentId();
if (ret === 'startPage') { if (currentId === 'startPage') {
ret = null; currentId = null;
} }
return currentId;
return ret;
}, },
getCurrentPath: function () { getCurrentPath: function () {
var currentPath = $(".edit-panel .tabs .current span:eq(0)").attr("title"); var currentPath = $(".edit-panel .tabs .current span:eq(0)").attr("title");
@ -299,7 +156,6 @@ var editors = {
}, },
_initCodeMirrorHotKeys: function () { _initCodeMirrorHotKeys: function () {
CodeMirror.registerHelper("hint", "go", function (editor) { CodeMirror.registerHelper("hint", "go", function (editor) {
editor = wide.curEditor; // 使用当前编辑器覆盖实参,因为异步调用的原因,实参不一定正确
var word = /[\w$]+/; var word = /[\w$]+/;
var cur = editor.getCursor(), curLine = editor.getLine(cur.line); var cur = editor.getCursor(), curLine = editor.getLine(cur.line);
@ -320,12 +176,6 @@ var editors = {
var autocompleteHints = []; var autocompleteHints = [];
if (editors.autocompleteMutex && editor.state.completionActive) {
return;
}
editors.autocompleteMutex = true;
$.ajax({ $.ajax({
async: false, // 同步执行 async: false, // 同步执行
type: 'POST', type: 'POST',
@ -337,72 +187,43 @@ var editors = {
if (autocompleteArray) { if (autocompleteArray) {
for (var i = 0; i < autocompleteArray.length; i++) { for (var i = 0; i < autocompleteArray.length; i++) {
var displayText = '', var displayText = '';
text = autocompleteArray[i].name;
switch (autocompleteArray[i].class) { switch (autocompleteArray[i].class) {
case "type": case "type":
displayText = '<span class="fn-clear"><span class="ico-type ico"></span>'// + autocompleteArray[i].class
+ '<b>' + autocompleteArray[i].name + '</b> '
+ autocompleteArray[i].type + '</span>';
break;
case "const": case "const":
displayText = '<span class="fn-clear"><span class="ico-const ico"></span>'// + autocompleteArray[i].class
+ '<b>' + autocompleteArray[i].name + '</b> '
+ autocompleteArray[i].type + '</span>';
break;
case "var": case "var":
displayText = '<span class="fn-clear"><span class="ico-var ico"></span>'// + autocompleteArray[i].class
+ '<b>' + autocompleteArray[i].name + '</b> '
+ autocompleteArray[i].type + '</span>';
break;
case "package": case "package":
displayText = '<span class="fn-clear"><span class="ico-package ico"></span>'// + autocompleteArray[i].class displayText = '<span class="fn-clear">'// + autocompleteArray[i].class
+ '<b>' + autocompleteArray[i].name + '</b> ' + '<b class="fn-left">' + autocompleteArray[i].name + '</b> '
+ autocompleteArray[i].type + '</span>'; + autocompleteArray[i].type + '</span>';
break; break;
case "func": case "func":
displayText = '<span><span class="ico-func ico"></span>'// + autocompleteArray[i].class displayText = '<span>'// + autocompleteArray[i].class
+ '<b>' + autocompleteArray[i].name + '</b>' + '<b>' + autocompleteArray[i].name + '</b>'
+ autocompleteArray[i].type.substring(4) + '</span>'; + autocompleteArray[i].type.substring(4) + '</span>';
text += '()';
break; break;
default: default:
console.warn("Can't handle autocomplete [" + autocompleteArray[i].class + "]"); console.warn("Can't handle autocomplete [" + autocompleteArray[i].class + "]");
break; break;
} }
autocompleteHints[i] = { autocompleteHints[i] = {
displayText: displayText, displayText: displayText,
text: text text: autocompleteArray[i].name
}; };
} }
} }
editor.doc.markClean();
$(".edit-panel .tabs .current > span:eq(0)").removeClass("changed");
} }
}); });
setTimeout(function () {
editors.autocompleteMutex = false;
}, 20);
return {list: autocompleteHints, from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end)}; return {list: autocompleteHints, from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end)};
}); });
CodeMirror.commands.autocompleteAfterDot = function (cm) { CodeMirror.commands.autocompleteAfterDot = function (cm) {
var mode = cm.getMode();
if (mode && "go" !== mode.name) {
return CodeMirror.Pass;
}
var token = cm.getTokenAt(cm.getCursor());
if ("comment" === token.type || "string" === token.type) {
return CodeMirror.Pass;
}
setTimeout(function () { setTimeout(function () {
if (!cm.state.completionActive) { if (!cm.state.completionActive) {
cm.showHint({hint: CodeMirror.hint.go, completeSingle: false}); cm.showHint({hint: CodeMirror.hint.go, completeSingle: false});
@ -438,140 +259,18 @@ var editors = {
url: '/exprinfo', url: '/exprinfo',
data: JSON.stringify(request), data: JSON.stringify(request),
dataType: "json", dataType: "json",
success: function (result) { success: function (data) {
if (0 != result.code) { if (!data.succ) {
return; return;
} }
var position = wide.curEditor.cursorCoords(); var position = wide.curEditor.cursorCoords();
$("body").append('<div style="top:' $("body").append('<div style="top:'
+ (position.top + 15) + 'px;left:' + position.left + (position.top + 15) + 'px;left:' + position.left
+ 'px" class="edit-exprinfo">' + result.data + '</div>'); + 'px" class="edit-exprinfo">' + data.info + '</div>');
} }
}); });
}; };
CodeMirror.commands.copyLinesDown = function (cm) {
var content = '',
selectoion = cm.listSelections()[0];
var from = selectoion.anchor,
to = selectoion.head;
if (from.line > to.line) {
from = selectoion.head;
to = selectoion.anchor;
}
for (var i = from.line, max = to.line; i <= max; i++) {
if (to.ch !== 0 || i !== max) { // 下一行选中为0时不应添加内容
content += '\n' + cm.getLine(i);
}
}
// 下一行选中为0时应添加到上一行末
var replaceToLine = to.line;
if (to.ch === 0) {
replaceToLine = to.line - 1;
}
cm.replaceRange(content, CodeMirror.Pos(replaceToLine));
var offset = replaceToLine - from.line + 1;
cm.setSelection(CodeMirror.Pos(from.line + offset, from.ch),
CodeMirror.Pos(to.line + offset, to.ch));
};
CodeMirror.commands.copyLinesUp = function (cm) {
var content = '',
selectoion = cm.listSelections()[0];
var from = selectoion.anchor,
to = selectoion.head;
if (from.line > to.line) {
from = selectoion.head;
to = selectoion.anchor;
}
for (var i = from.line, max = to.line; i <= max; i++) {
if (to.ch !== 0 || i !== max) { // 下一行选中为0时不应添加内容
content += '\n' + cm.getLine(i);
}
}
// 下一行选中为0时应添加到上一行末
var replaceToLine = to.line;
if (to.ch === 0) {
replaceToLine = to.line - 1;
}
cm.replaceRange(content, CodeMirror.Pos(replaceToLine));
cm.setSelection(CodeMirror.Pos(from.line, from.ch),
CodeMirror.Pos(to.line, to.ch));
};
CodeMirror.commands.moveLinesUp = function (cm) {
var selectoion = cm.listSelections()[0];
var from = selectoion.anchor,
to = selectoion.head;
if (from.line > to.line) {
from = selectoion.head;
to = selectoion.anchor;
}
if (from.line === 0) {
return false;
}
// 下一行选中为0时应添加到上一行末
var replaceToLine = to.line;
if (to.ch === 0) {
replaceToLine = to.line - 1;
}
cm.replaceRange('\n' + cm.getLine(from.line - 1), CodeMirror.Pos(replaceToLine));
if (from.line === 1) {
// 移除第一行的换行
cm.replaceRange('', CodeMirror.Pos(0, 0),
CodeMirror.Pos(1, 0));
} else {
cm.replaceRange('', CodeMirror.Pos(from.line - 2, cm.getLine(from.line - 2).length),
CodeMirror.Pos(from.line - 1, cm.getLine(from.line - 1).length));
}
cm.setSelection(CodeMirror.Pos(from.line - 1, from.ch),
CodeMirror.Pos(to.line - 1, to.ch));
};
CodeMirror.commands.moveLinesDown = function (cm) {
var selectoion = cm.listSelections()[0];
var from = selectoion.anchor,
to = selectoion.head;
if (from.line > to.line) {
from = selectoion.head;
to = selectoion.anchor;
}
if (to.line === cm.lastLine()) {
return false;
}
// 下一行选中为0时应添加到上一行末
var replaceToLine = to.line;
if (to.ch === 0) {
replaceToLine = to.line - 1;
}
// 把选中的下一行添加到选中区域的上一行
if (from.line === 0) {
cm.replaceRange(cm.getLine(replaceToLine + 1) + '\n', CodeMirror.Pos(0, 0));
} else {
cm.replaceRange('\n' + cm.getLine(replaceToLine + 1), CodeMirror.Pos(from.line - 1));
}
// 删除选中的下一行
cm.replaceRange('', CodeMirror.Pos(replaceToLine + 1, cm.getLine(replaceToLine + 1).length),
CodeMirror.Pos(replaceToLine + 2, cm.getLine(replaceToLine + 2).length));
cm.setSelection(CodeMirror.Pos(from.line + 1, from.ch),
CodeMirror.Pos(to.line + 1, to.ch));
};
CodeMirror.commands.jumpToDecl = function (cm) { CodeMirror.commands.jumpToDecl = function (cm) {
var cur = wide.curEditor.getCursor(); var cur = wide.curEditor.getCursor();
@ -586,18 +285,38 @@ var editors = {
url: '/find/decl', url: '/find/decl',
data: JSON.stringify(request), data: JSON.stringify(request),
dataType: "json", dataType: "json",
success: function (result) { success: function (data) {
if (0 != result.code) { if (!data.succ) {
return; return;
} }
var data = result.data;
var tId = tree.getTIdByPath(data.path); var cursorLine = data.cursorLine;
wide.curNode = tree.fileTree.getNodeByTId(tId); var cursorCh = data.cursorCh;
tree.fileTree.selectNode(wide.curNode);
tree.openFile(wide.curNode, CodeMirror.Pos(data.cursorLine - 1, data.cursorCh - 1)); var request = newWideRequest();
request.path = data.path;
$.ajax({
type: 'POST',
url: '/file',
data: JSON.stringify(request),
dataType: "json",
success: function (data) {
if (!data.succ) {
$("#dialogAlert").dialog("open", data.msg);
return false;
}
var tId = tree.getTIdByPath(data.path);
wide.curNode = tree.fileTree.getNodeByTId(tId);
tree.fileTree.selectNode(wide.curNode);
data.cursorLine = cursorLine;
data.cursorCh = cursorCh;
editors.newEditor(data);
}
});
} }
}); });
}; };
@ -616,52 +335,32 @@ var editors = {
url: '/find/usages', url: '/find/usages',
data: JSON.stringify(request), data: JSON.stringify(request),
dataType: "json", dataType: "json",
success: function (result) { success: function (data) {
if (0 != result.code) { if (!data.succ) {
return; return;
} }
editors.appendSearch(result.data, 'usages', ''); editors.appendSearch(data.founds, 'usages', '');
} }
}); });
}; };
CodeMirror.commands.selectIdentifier = function (cm) {
var cur = cm.getCursor();
var word = cm.findWordAt(cur);
cm.extendSelection(word.anchor, word.head);
};
}, },
appendSearch: function (data, type, key) { appendSearch: function (data, type, key) {
var searcHTML = '<ul class="list">', var searcHTML = '<ul>';
key = key.toLowerCase();
for (var i = 0, ii = data.length; i < ii; i++) { for (var i = 0, ii = data.length; i < ii; i++) {
var contents = '', var contents = data[i].contents[0],
lowerCaseContents = data[i].contents[0].toLowerCase(), index = contents.indexOf(key);
matches = lowerCaseContents.split(key), contents = contents.substring(0, index)
startIndex = 0, + '<b>' + key + '</b>'
endIndex = 0; + contents.substring(index + key.length);
for (var j = 0, max = matches.length; j < max; j++) {
startIndex = endIndex + matches[j].length;
endIndex = startIndex + key.length;
var keyWord = data[i].contents[0].substring(startIndex, endIndex);
if (keyWord !== '') {
keyWord = '<b>' + keyWord + '</b>';
}
contents += data[i].contents[0].substring(startIndex - matches[j].length, startIndex) + keyWord;
}
searcHTML += '<li title="' + data[i].path + '">' searcHTML += '<li title="' + data[i].path + '">'
+ contents + "&nbsp;&nbsp;&nbsp;&nbsp;<span class='ft-small'>" + data[i].path + contents + "&nbsp;&nbsp;&nbsp;&nbsp;<span class='path'>" + data[i].path
+ '<i class="position" data-line="' + '<i class="position" data-line="'
+ data[i].line + '" data-ch="' + data[i].ch + '"> (' + data[i].line + ':' + data[i].line + '" data-ch="' + data[i].ch + '"> (' + data[i].line + ':'
+ data[i].ch + ')</i></span></li>'; + data[i].ch + ')</i></span></li>';
} }
if (data.length === 0) {
searcHTML += '<li>' + config.label.search_no_match + '</li>';
}
searcHTML += '</ul>'; searcHTML += '</ul>';
var $search = $('.bottom-window-group .search'), var $search = $('.bottom-window-group .search'),
@ -670,7 +369,7 @@ var editors = {
title = config.label.search_text; title = config.label.search_text;
} }
if ($search.find("ul").length === 0) { if ($search.find("ul").length === 0) {
bottomGroup.searchTab = new Tabs({ wide.searchTab = new Tabs({
id: ".bottom-window-group .search", id: ".bottom-window-group .search",
removeAfter: function (id, prevId) { removeAfter: function (id, prevId) {
if ($search.find("ul").length === 1) { if ($search.find("ul").length === 1) {
@ -690,17 +389,8 @@ var editors = {
tree.openFile(tree.fileTree.getNodeByTId(tId)); tree.openFile(tree.fileTree.getNodeByTId(tId));
tree.fileTree.selectNode(wide.curNode); tree.fileTree.selectNode(wide.curNode);
var line = $it.find(".position").data("line") - 1; var cursor = CodeMirror.Pos($it.find(".position").data("line") - 1, $it.find(".position").data("ch") - 1);
var cursor = CodeMirror.Pos(line, $it.find(".position").data("ch") - 1); wide.curEditor.setCursor(cursor);
var editor = wide.curEditor;
editor.setCursor(cursor);
var half = Math.floor(editor.getScrollInfo().clientHeight / editor.defaultTextHeight() / 2);
var cursorCoords = editor.cursorCoords({line: cursor.line - half, ch: 0}, "local");
editor.scrollTo(0, cursorCoords.top);
wide.curEditor.focus(); wide.curEditor.focus();
}); });
@ -709,7 +399,7 @@ var editors = {
$search.find(".tabs .first").text(title); $search.find(".tabs .first").text(title);
} else { } else {
$search.find(".tabs").show(); $search.find(".tabs").show();
bottomGroup.searchTab.add({ wide.searchTab.add({
"id": "search" + (new Date()).getTime(), "id": "search" + (new Date()).getTime(),
"title": title, "title": title,
"content": searcHTML "content": searcHTML
@ -717,13 +407,31 @@ var editors = {
} }
// focus // focus
bottomGroup.tabs.setCurrent("search"); wide.bottomWindowTab.setCurrent("search");
windows.flowBottom(); windows.flowBottom();
$(".bottom-window-group .search").focus(); $(".bottom-window-group .search").focus();
}, },
// 新建一个编辑器 Tab如果已经存在 Tab 则切换到该 Tab. // 新建一个编辑器 Tab如果已经存在 Tab 则切换到该 Tab.
newEditor: function (data, cursor) { newEditor: function (data) {
var id = wide.curNode.id; $(".toolbars").show();
var id = wide.curNode.tId;
// 光标位置
var cursor = CodeMirror.Pos(0, 0);
if (data.cursorLine && data.cursorCh) {
cursor = CodeMirror.Pos(data.cursorLine - 1, data.cursorCh - 1);
}
for (var i = 0, ii = editors.data.length; i < ii; i++) {
if (editors.data[i].id === id) {
editors.tabs.setCurrent(id);
wide.curEditor = editors.data[i].editor;
wide.curEditor.setCursor(cursor);
wide.curEditor.focus();
return false;
}
}
editors.tabs.add({ editors.tabs.add({
id: id, id: id,
@ -732,35 +440,25 @@ var editors = {
content: '<textarea id="editor' + id + '"></textarea>' content: '<textarea id="editor' + id + '"></textarea>'
}); });
menu.undisabled(['save-all', 'close-all', 'build', 'run', 'go-test', 'go-vet', 'go-mod', 'go-install', menu.undisabled(['save-all', 'close-all', 'run', 'go-get', 'go-install']);
'find', 'find-next', 'find-previous', 'replace', 'replace-all',
'format', 'autocomplete', 'jump-to-decl', 'expr-info', 'find-usages', 'toggle-comment',
'edit']);
var textArea = document.getElementById("editor" + id); var rulers = [];
textArea.value = data.content; rulers.push({color: "#ccc", column: 120, lineStyle: "dashed"});
var editor = CodeMirror.fromTextArea(textArea, { var editor = CodeMirror.fromTextArea(document.getElementById("editor" + id), {
lineNumbers: true, lineNumbers: true,
autofocus: true, autofocus: true,
autoCloseBrackets: true, autoCloseBrackets: true,
matchBrackets: true, matchBrackets: true,
highlightSelectionMatches: {showToken: /\w/}, highlightSelectionMatches: {showToken: /\w/},
rulers: [{color: "#ccc", column: 120, lineStyle: "dashed"}], rulers: rulers,
styleActiveLine: true, styleActiveLine: true,
theme: config.editorTheme, theme: 'wide',
tabSize: config.editorTabSize,
indentUnit: 4, indentUnit: 4,
indentWithTabs: true,
foldGutter: true, foldGutter: true,
cursorHeight: 1,
path: data.path,
readOnly: wide.curNode.isGOAPI,
profile: 'xhtml', // define Emmet output profile
extraKeys: { extraKeys: {
"Ctrl-\\": "autocompleteAnyWord", "Ctrl-\\": "autocompleteAnyWord",
".": "autocompleteAfterDot", ".": "autocompleteAfterDot",
"Ctrl-/": 'toggleComment',
"Ctrl-I": "exprInfo", "Ctrl-I": "exprInfo",
"Ctrl-L": "gotoLine", "Ctrl-L": "gotoLine",
"Ctrl-E": "deleteLine", "Ctrl-E": "deleteLine",
@ -770,7 +468,7 @@ var editors = {
wide.saveFile(); wide.saveFile();
}, },
"Shift-Ctrl-S": function () { "Shift-Ctrl-S": function () {
menu.saveAllFiles(); wide.saveAllFiles();
}, },
"Shift-Alt-F": function () { "Shift-Alt-F": function () {
var currentPath = editors.getCurrentPath(); var currentPath = editors.getCurrentPath();
@ -787,89 +485,30 @@ var editors = {
windows.maxEditor(); windows.maxEditor();
} }
}, },
"Shift-Ctrl-Up": "copyLinesUp",
"Shift-Ctrl-Down": "copyLinesDown",
"Shift-Alt-Up": "moveLinesUp",
"Shift-Alt-Down": "moveLinesDown",
"Shift-Alt-J": "selectIdentifier"
} }
}); });
if ("text/html" === data.mode) {
emmetCodeMirror(editor);
}
editor.on('cursorActivity', function (cm) { editor.on('cursorActivity', function (cm) {
$(".edit-exprinfo").remove(); $(".edit-exprinfo").remove();
var cursor = cm.getCursor(); var cursor = cm.getCursor();
$(".footer .cursor").text('| ' + (cursor.line + 1) + ':' + (cursor.ch + 1) + ' |'); $(".footer .cursor").text('| ' + (cursor.line + 1) + ':' + (cursor.ch + 1) + ' |');
// TODO: 关闭 tab 的时候要重置
});
editor.on('focus', function (cm) {
windows.clearFloat();
}); });
editor.on('blur', function (cm) { editor.on('blur', function (cm) {
$(".edit-exprinfo").remove(); $(".edit-exprinfo").remove();
}); });
editor.on('changes', function (cm) {
if (cm.doc.isClean()) { // no modification
$(".edit-panel .tabs > div").each(function () {
var $span = $(this).find("span:eq(0)");
if ($span.attr("title") === cm.options.path) {
$span.removeClass("changed");
}
});
return;
}
// changed
$(".edit-panel .tabs > div").each(function () {
var $span = $(this).find("span:eq(0)");
if ($span.attr("title") === cm.options.path) {
$span.addClass("changed");
}
});
});
editor.on('keydown', function (cm, evt) {
if (evt.altKey || evt.ctrlKey || evt.shiftKey) {
return;
}
var k = evt.which;
if (k < 48) {
return;
}
// hit [0-9]
if (k > 57 && k < 65) {
return;
}
// hit [a-z]
if (k > 90) {
return;
}
if (config.autocomplete) {
if (0.5 <= Math.random()) {
CodeMirror.commands.autocompleteAfterDot(cm);
}
}
});
editor.setSize('100%', $(".edit-panel").height() - $(".edit-panel .tabs").height()); editor.setSize('100%', $(".edit-panel").height() - $(".edit-panel .tabs").height());
editor.setValue(data.content);
editor.setOption("mode", data.mode); editor.setOption("mode", data.mode);
editor.setOption("gutters", ["CodeMirror-lint-markers", "CodeMirror-foldgutter"]); editor.setOption("gutters", ["CodeMirror-lint-markers", "CodeMirror-foldgutter"]);
if ("wide" !== config.keymap) {
editor.setOption("keyMap", config.keymap);
}
if ("text/x-go" === data.mode || "application/json" === data.mode) { if ("text/x-go" === data.mode || "application/json" === data.mode) {
editor.setOption("lint", true); editor.setOption("lint", true);
} }
@ -878,19 +517,12 @@ var editors = {
editor.setOption("autoCloseTags", true); editor.setOption("autoCloseTags", true);
} }
editor.setCursor(cursor);
wide.curEditor = editor; wide.curEditor = editor;
editors.data.push({ editors.data.push({
"editor": editor, "editor": editor,
"id": id "id": id
}); });
$(".footer .cursor").text('| ' + (cursor.line + 1) + ':' + (cursor.ch + 1) + ' |');
var half = Math.floor(wide.curEditor.getScrollInfo().clientHeight / wide.curEditor.defaultTextHeight() / 2);
var cursorCoords = wide.curEditor.cursorCoords({line: cursor.line - half, ch: 0}, "local");
wide.curEditor.scrollTo(0, cursorCoords.top);
editor.setCursor(cursor);
editor.focus();
} }
}; };

View File

@ -1,154 +1,62 @@
/*
* Copyright (c) 2014-present, b3log.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* @file hotkeys.js
*
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
* @author <a href="http://88250.b3log.org">Liang Ding</a>
* @version 1.0.0.2, Dec 15, 2015
*/
var hotkeys = { var hotkeys = {
defaultKeyMap: { defaultKeyMap: {
// Ctrl-0 // Ctrl+0 焦点切换到当前编辑器
goEditor: { goEditor: {
ctrlKey: true, ctrlKey: true,
altKey: false, altKey: false,
shiftKey: false, shiftKey: false,
which: 48, which: 48
fun: function () {
if (wide.curEditor) {
wide.curEditor.focus();
}
}
}, },
// Ctrl-1 // Ctrl+1 焦点切换到文件树
goFileTree: { goFileTree: {
ctrlKey: true, ctrlKey: true,
altKey: false, altKey: false,
shiftKey: false, shiftKey: false,
which: 49, which: 49
fun: function () {
// 有些元素需设置 tabindex 为 -1 时才可以 focus
if (windows.outerLayout.west.state.isClosed) {
windows.outerLayout.slideOpen('west');
}
$("#files").focus();
}
}, },
// Ctrl-2 // Ctrl+4 焦点切换到输出窗口
goOutline: { goOutPut: {
ctrlKey: true, ctrlKey: true,
altKey: false, altKey: false,
shiftKey: false, shiftKey: false,
which: 50, which: 52
fun: function () {
if (windows.innerLayout.east.state.isClosed) {
windows.innerLayout.slideOpen('east');
}
$("#outline").focus();
}
}, },
// Ctrl-4 // Ctrl+5 焦点切换到搜索窗口
goOutput: {
ctrlKey: true,
altKey: false,
shiftKey: false,
which: 52,
fun: function () {
bottomGroup.tabs.setCurrent("output");
windows.flowBottom();
$(".bottom-window-group .output").focus();
}
},
// Ctrl-5
goSearch: { goSearch: {
ctrlKey: true, ctrlKey: true,
altKey: false, altKey: false,
shiftKey: false, shiftKey: false,
which: 53, which: 53
fun: function () {
bottomGroup.tabs.setCurrent("search");
windows.flowBottom();
$(".bottom-window-group .search").focus();
}
}, },
// Ctrl-6 // Ctrl+6 焦点切换到通知窗口
goNotification: { goNotification: {
ctrlKey: true, ctrlKey: true,
altKey: false, altKey: false,
shiftKey: false, shiftKey: false,
which: 54, which: 54
fun: function () {
bottomGroup.tabs.setCurrent("notification");
windows.flowBottom();
$(".bottom-window-group .notification").focus();
}
}, },
// Alt-C // Ctrl+D 窗口组切换
clearWindow: {
ctrlKey: false,
altKey: true,
shiftKey: false,
which: 67
},
// Ctrl-D 窗口组切换
changeEditor: { changeEditor: {
ctrlKey: true, ctrlKey: true,
altKey: false, altKey: false,
shiftKey: false, shiftKey: false,
which: 68 which: 68
}, },
// Ctrl-F search // Ctrl+F 搜索
search: { search: {
ctrlKey: true, ctrlKey: true,
altKey: false, altKey: false,
shiftKey: false, shiftKey: false,
which: 70 which: 70
}, },
// Ctrl-Q close current editor // Ctrl+Q 关闭当前编辑器
closeCurEditor: { closeCurEditor: {
ctrlKey: true, ctrlKey: true,
altKey: false, altKey: false,
shiftKey: false, shiftKey: false,
which: 81 which: 81
}, },
// Ctrl-R // F6 构建并运行
rename: {
ctrlKey: true,
altKey: false,
shiftKey: false,
which: 82
},
// Shift-Alt-O 跳转到文件
goFile: {
ctrlKey: false,
altKey: true,
shiftKey: true,
which: 79
},
// F5 Build
build: {
ctrlKey: false,
altKey: false,
shiftKey: false,
which: 116
},
// F6 Build & Run
buildRun: { buildRun: {
ctrlKey: false, ctrlKey: false,
altKey: false, altKey: false,
@ -156,132 +64,56 @@ var hotkeys = {
which: 117 which: 117
} }
}, },
bindList: function ($source, $list, enterFun) {
$list.data("index", 0);
$source.keydown(function (event) {
var index = $list.data("index"),
count = $list.find("li").length;
if (count === 0) {
return true;
}
if (event.which === 38) { // up
index--;
if (index < 0) {
index = count - 1;
}
}
if (event.which === 40) { // down
index++;
if (index > count - 1) {
index = 0;
}
}
var $selected = $list.find("li:eq(" + index + ")");
if (event.which === 13) { // enter
enterFun($selected);
}
$list.find("li").removeClass("selected");
$list.data("index", index);
$selected.addClass("selected");
if (index === 0) {
$list.scrollTop(0);
} else {
if ($selected[0].offsetTop + $list.scrollTop() > $list.height()) {
if (event.which === 40) {
$list.scrollTop($list.scrollTop() + $selected.height());
} else {
$list.scrollTop($selected[0].offsetTop);
}
} else {
$list.scrollTop(0);
}
}
// 阻止上下键改变光标位置
if (event.which === 38 || event.which === 40 || event.which === 13) {
return false;
}
});
},
_bindOutput: function () {
$(".bottom-window-group .output").keydown(function (event) {
var hotKeys = hotkeys.defaultKeyMap;
if (event.altKey === hotKeys.clearWindow.altKey
&& event.which === hotKeys.clearWindow.which) { // Alt-C clear output
bottomGroup.clear('output');
event.preventDefault();
return;
}
});
},
_bindFileTree: function () { _bindFileTree: function () {
$("#files").keydown(function (event) { $("#files").keydown(function (event) {
event.preventDefault(); event.preventDefault();
var hotKeys = hotkeys.defaultKeyMap; var hotKeys = hotkeys.defaultKeyMap;
if (event.ctrlKey === hotKeys.search.ctrlKey if (event.ctrlKey === hotKeys.search.ctrlKey
&& event.which === hotKeys.search.which) { // Ctrl-F 搜索 && event.which === hotKeys.search.which) { // Ctrl+F 搜索
$("#dialogSearchForm").dialog("open"); $("#dialogSearchForm").dialog("open");
return; return;
} }
if (event.ctrlKey === hotKeys.rename.ctrlKey
&& event.which === hotKeys.rename.which) { // Ctrl-R 重命名
if (wide.curNode.removable) {
$("#dialogRenamePrompt").dialog("open");
}
return;
}
switch (event.which) { switch (event.which) {
case 46: // delete case 46: // 删除
tree.removeIt(); tree.removeIt();
break; break;
case 13: // enter case 13: // 回车
if (!wide.curNode) { if (!wide.curNode) {
return false; return false;
} }
if (tree.isDir()) { if (wide.curNode.iconSkin === "ico-ztree-dir ") { // 选中节点是目录
if (wide.curNode.open) { // 不做任何处理
return false; return false;
}
tree.fileTree.expandNode(wide.curNode, true, false, true);
$("#files").focus();
break;
} }
// 模拟点击:打开文件
tree.openFile(wide.curNode); tree.openFile(wide.curNode);
break; break;
case 38: // up case 38: // 上
var node = {}; var node = {};
if (!wide.curNode) { // select the first one if no node been selected if (!wide.curNode) { // 没有选中节点时,默认选中第一个
node = tree.fileTree.getNodeByTId("files_1"); node = tree.fileTree.getNodeByTId("files_1");
} else { } else {
if (wide.curNode && wide.curNode.isFirstNode && wide.curNode.level === 0) { if (wide.curNode && wide.curNode.isFirstNode && wide.curNode.level === 0) {
// 当前节点为顶部第一个节点
return false; return false;
} }
node = wide.curNode.getPreNode(); node = wide.curNode.getPreNode();
if (wide.curNode.isFirstNode && wide.curNode.getParentNode()) { if (wide.curNode.isFirstNode && wide.curNode.getParentNode()) {
// 当前节点为第一个节点且有父亲
node = wide.curNode.getParentNode(); node = wide.curNode.getParentNode();
} }
var preNode = wide.curNode.getPreNode(); var preNode = wide.curNode.getPreNode();
if (preNode && tree.isDir() && preNode.open) { if (preNode && preNode.iconSkin === "ico-ztree-dir "
&& preNode.open) {
// 当前节点的上一个节点是目录且打开时,获取打开节点中的最后一个节点
node = tree.getCurrentNodeLastNode(preNode); node = tree.getCurrentNodeLastNode(preNode);
} }
} }
@ -290,23 +122,27 @@ var hotkeys = {
tree.fileTree.selectNode(node); tree.fileTree.selectNode(node);
$("#files").focus(); $("#files").focus();
break; break;
case 40: // down case 40: //
var node = {}; var node = {};
if (!wide.curNode) { // select the first one if no node been selected if (!wide.curNode) { // 没有选中节点时,默认选中第一个
node = tree.fileTree.getNodeByTId("files_1"); node = tree.fileTree.getNodeByTId("files_1");
} else { } else {
if (wide.curNode && tree.isBottomNode(wide.curNode)) { if (wide.curNode && tree.isBottomNode(wide.curNode)) {
// 当前节点为最底部的节点
return false; return false;
} }
node = wide.curNode.getNextNode(); node = wide.curNode.getNextNode();
if (tree.isDir() && wide.curNode.open) { if (wide.curNode.iconSkin === "ico-ztree-dir " && wide.curNode.open) {
// 当前节点是目录且打开时
node = wide.curNode.children[0]; node = wide.curNode.children[0];
} }
var nextShowNode = tree.getNextShowNode(wide.curNode); var nextShowNode = tree.getNextShowNode(wide.curNode);
if (wide.curNode.isLastNode && wide.curNode.level !== 0 && !wide.curNode.open && nextShowNode) { if (wide.curNode.isLastNode && wide.curNode.level !== 0 && !wide.curNode.open
&& nextShowNode) {
// 当前节点为最后一个叶子节点,但其父或祖先节点还有下一个节点
node = nextShowNode; node = nextShowNode;
} }
} }
@ -318,7 +154,7 @@ var hotkeys = {
$("#files").focus(); $("#files").focus();
break; break;
case 37: // left case 37: //
if (!wide.curNode) { if (!wide.curNode) {
wide.curNode = tree.fileTree.getNodeByTId("files_1"); wide.curNode = tree.fileTree.getNodeByTId("files_1");
tree.fileTree.selectNode(wide.curNode); tree.fileTree.selectNode(wide.curNode);
@ -326,14 +162,14 @@ var hotkeys = {
return false; return false;
} }
if (!tree.isDir() || !wide.curNode.open) { if (wide.curNode.iconSkin !== "ico-ztree-dir " || !wide.curNode.open) {
return false; return false;
} }
tree.fileTree.expandNode(wide.curNode, false, false, true); tree.fileTree.expandNode(wide.curNode, false, false, true);
$("#files").focus(); $("#files").focus();
break; break;
case 39: // right case 39: //
if (!wide.curNode) { if (!wide.curNode) {
wide.curNode = tree.fileTree.getNodeByTId("files_1"); wide.curNode = tree.fileTree.getNodeByTId("files_1");
tree.fileTree.selectNode(wide.curNode); tree.fileTree.selectNode(wide.curNode);
@ -341,104 +177,114 @@ var hotkeys = {
return false; return false;
} }
if (!tree.isDir() || wide.curNode.open) { if (wide.curNode.iconSkin !== "ico-ztree-dir " || wide.curNode.open) {
return false; return false;
} }
tree.fileTree.expandNode(wide.curNode, true, false, true); tree.fileTree.expandNode(wide.curNode, true, false, true);
$("#files").focus(); $("#files").focus();
break;
case 116: // F5
if (!wide.curNode || !tree.isDir()) {
return false;
}
tree.refresh(wide.curNode);
break; break;
} }
}); });
}, },
_bindDocument: function () { init: function () {
this._bindFileTree();
var hotKeys = this.defaultKeyMap; var hotKeys = this.defaultKeyMap;
$(document).keydown(function (event) { $(document).keydown(function (event) {
if (event.ctrlKey === hotKeys.goEditor.ctrlKey if (event.ctrlKey === hotKeys.goEditor.ctrlKey
&& event.which === hotKeys.goEditor.which) { // Ctrl-0 焦点切换到当前编辑器 && event.which === hotKeys.goEditor.which) { // Ctrl+0 焦点切换到当前编辑器
hotKeys.goEditor.fun(); if (wide.curEditor) {
wide.curEditor.focus();
}
event.preventDefault(); event.preventDefault();
return; return;
} }
if (event.ctrlKey === hotKeys.goFileTree.ctrlKey if (event.ctrlKey === hotKeys.goFileTree.ctrlKey
&& event.which === hotKeys.goFileTree.which) { // Ctrl-1 焦点切换到文件树 && event.which === hotKeys.goFileTree.which) { // Ctrl+1 焦点切换到文件树
hotKeys.goFileTree.fun(); // 有些元素需设置 tabindex 为 -1 时才可以 focus
if ($(".footer .ico-restore:eq(0)").css("display") === "inline") {
// 当文件树最小化时
$(".side").css({
"left": "0"
});
if ($(".footer .ico-restore:eq(1)").css("display") === "inline") {
// 当底部最小化时
$(".bottom-window-group").css("top", "100%").hide();
}
}
$("#files").focus();
event.preventDefault(); event.preventDefault();
return; return;
} }
if (event.ctrlKey === hotKeys.goOutline.ctrlKey if (event.ctrlKey === hotKeys.goOutPut.ctrlKey
&& event.which === hotKeys.goOutline.which) { // Ctrl-2 焦点切换到大纲 && event.which === hotKeys.goOutPut.which) { // Ctrl+4 焦点切换到输出窗口
hotKeys.goOutline.fun(); wide.bottomWindowTab.setCurrent("output");
windows.flowBottom();
$(".bottom-window-group .output").focus();
event.preventDefault(); event.preventDefault();
return; return;
} }
if (event.ctrlKey === hotKeys.goOutput.ctrlKey
&& event.which === hotKeys.goOutput.which) { // Ctrl-4 焦点切换到输出窗口
hotKeys.goOutput.fun();
event.preventDefault();
return;
}
if (event.ctrlKey === hotKeys.goSearch.ctrlKey if (event.ctrlKey === hotKeys.goSearch.ctrlKey
&& event.which === hotKeys.goSearch.which) { // Ctrl-5 焦点切换到搜索窗口 && event.which === hotKeys.goSearch.which) { // Ctrl+5 焦点切换到搜索窗口
hotKeys.goSearch.fun(); wide.bottomWindowTab.setCurrent("search");
windows.flowBottom();
$(".bottom-window-group .search").focus();
event.preventDefault(); event.preventDefault();
return; return;
} }
if (event.ctrlKey === hotKeys.goNotification.ctrlKey if (event.ctrlKey === hotKeys.goNotification.ctrlKey
&& event.which === hotKeys.goNotification.which) { // Ctrl-6 焦点切换到通知窗口 && event.which === hotKeys.goNotification.which) { // Ctrl+6 焦点切换到通知窗口
hotKeys.goNotification.fun(); wide.bottomWindowTab.setCurrent("notification");
windows.flowBottom();
$(".bottom-window-group .notification").focus();
event.preventDefault(); event.preventDefault();
return; return;
} }
if (event.ctrlKey === hotKeys.closeCurEditor.ctrlKey if (event.ctrlKey === hotKeys.closeCurEditor.ctrlKey
&& event.which === hotKeys.closeCurEditor.which) { // Ctrl-Q 关闭当前编辑器 && event.which === hotKeys.closeCurEditor.which) { // Ctrl+Q 关闭当前编辑器
$(".edit-panel .tabs > div.current").find(".ico-close").click(); var currentId = editors.getCurrentId();
if (currentId) {
editors.tabs.del(currentId);
}
event.preventDefault(); event.preventDefault();
return; return;
} }
if (event.ctrlKey === hotKeys.changeEditor.ctrlKey if (event.ctrlKey === hotKeys.changeEditor.ctrlKey
&& event.which === hotKeys.changeEditor.which) { // Ctrl-D 窗口组切换 && event.which === hotKeys.changeEditor.which) { // Ctrl+D 窗口组切换
if (document.activeElement.className === "notification" if (document.activeElement.className === "notification"
|| document.activeElement.className === "output" || document.activeElement.className === "output"
|| document.activeElement.className === "search") { || document.activeElement.className === "search") {
// 焦点在底部窗口组时,对底部进行切换 // 焦点在底部窗口组时,对底部进行切换
var tabs = ["output", "search", "notification"], var tabs = ["output", "search", "notification"],
nextPath = ""; nextId = "";
for (var i = 0, ii = tabs.length; i < ii; i++) { for (var i = 0, ii = tabs.length; i < ii; i++) {
if (bottomGroup.tabs.getCurrentId() === tabs[i]) { if (document.activeElement.className === tabs[i]) {
if (i < ii - 1) { if (i < ii - 1) {
nextPath = tabs[i + 1]; nextId = tabs[i + 1];
} else { } else {
nextPath = tabs[0]; nextId = tabs[0];
} }
break; break;
} }
} }
bottomGroup.tabs.setCurrent(nextPath); wide.bottomWindowTab.setCurrent(nextId);
$(".bottom-window-group ." + nextPath).focus(); $(".bottom-window-group ." + nextId).focus();
event.preventDefault(); event.preventDefault();
@ -446,16 +292,16 @@ var hotkeys = {
} }
if (editors.data.length > 1) { if (editors.data.length > 1) {
var nextPath = ""; var nextId = "";
for (var i = 0, ii = editors.data.length; i < ii; i++) { for (var i = 0, ii = editors.data.length; i < ii; i++) {
var currentId = editors.getCurrentId(); var currentId = editors.getCurrentId();
if (currentId) { if (currentId) {
if (currentId === editors.data[i].id) { if (currentId === editors.data[i].id) {
if (i < ii - 1) { if (i < ii - 1) {
nextPath = editors.data[i + 1].id; nextId = editors.data[i + 1].id;
wide.curEditor = editors.data[i + 1].editor; wide.curEditor = editors.data[i + 1].editor;
} else { } else {
nextPath = editors.data[0].id; nextId = editors.data[0].id;
wide.curEditor = editors.data[0].editor; wide.curEditor = editors.data[0].editor;
} }
break; break;
@ -463,14 +309,10 @@ var hotkeys = {
} }
} }
editors.tabs.setCurrent(nextPath); editors.tabs.setCurrent(nextId);
var nextTId = tree.getTIdByPath(nextPath); wide.curNode = tree.fileTree.getNodeByTId(nextId);
wide.curNode = tree.fileTree.getNodeByTId(nextTId);
tree.fileTree.selectNode(wide.curNode); tree.fileTree.selectNode(wide.curNode);
wide.refreshOutline();
var cursor = wide.curEditor.getCursor();
$(".footer .cursor").text('| ' + (cursor.line + 1) + ':' + (cursor.ch + 1) + ' |');
wide.curEditor.focus(); wide.curEditor.focus();
} }
@ -479,31 +321,12 @@ var hotkeys = {
return false; return false;
} }
if (event.which === hotKeys.build.which) { // F5 Build if (event.which === hotKeys.buildRun.which) { // F6 构建并运行
menu.build(); wide.run();
event.preventDefault(); event.preventDefault();
return; return;
} }
if (event.which === hotKeys.buildRun.which) { // F6 Build & Run
menu.run();
event.preventDefault();
return;
}
if (event.ctrlKey === hotKeys.goFile.ctrlKey
&& event.altKey === hotKeys.goFile.altKey
&& event.shiftKey === hotKeys.goFile.shiftKey
&& event.which === hotKeys.goFile.which) { // Shift-Alt-O 跳转到文件
$("#dialogGoFilePrompt").dialog("open");
}
}); });
},
init: function () {
this._bindFileTree();
this._bindOutput();
this._bindDocument();
} }
}; };

47
static/js/lib.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -109,7 +109,7 @@
CodeMirror.defineExtension("uncomment", function(from, to, options) { CodeMirror.defineExtension("uncomment", function(from, to, options) {
if (!options) options = noOptions; if (!options) options = noOptions;
var self = this, mode = self.getModeAt(from); var self = this, mode = self.getModeAt(from);
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end); var end = Math.min(to.line, self.lastLine()), start = Math.min(from.line, end);
// Try finding line comments // Try finding line comments
var lineString = options.lineComment || mode.lineComment, lines = []; var lineString = options.lineComment || mode.lineComment, lines = [];

View File

@ -56,12 +56,7 @@
var inp = dialog.getElementsByTagName("input")[0], button; var inp = dialog.getElementsByTagName("input")[0], button;
if (inp) { if (inp) {
if (options.value) { if (options.value) inp.value = options.value;
inp.value = options.value;
if (options.selectValueOnOpen !== false) {
inp.select();
}
}
if (options.onInput) if (options.onInput)
CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);}); CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);});
@ -75,7 +70,7 @@
CodeMirror.e_stop(e); CodeMirror.e_stop(e);
close(); close();
} }
if (e.keyCode == 13) callback(inp.value, e); if (e.keyCode == 13) callback(inp.value);
}); });
if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close); if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
@ -134,8 +129,8 @@
CodeMirror.defineExtension("openNotification", function(template, options) { CodeMirror.defineExtension("openNotification", function(template, options) {
closeNotification(this, close); closeNotification(this, close);
var dialog = dialogDiv(this, template, options && options.bottom); var dialog = dialogDiv(this, template, options && options.bottom);
var duration = options && (options.duration === undefined ? 5000 : options.duration);
var closed = false, doneTimer; var closed = false, doneTimer;
var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000;
function close() { function close() {
if (closed) return; if (closed) return;
@ -148,10 +143,7 @@
CodeMirror.e_preventDefault(e); CodeMirror.e_preventDefault(e);
close(); close();
}); });
if (duration) if (duration)
doneTimer = setTimeout(close, duration); doneTimer = setTimeout(close, options.duration);
return close;
}); });
}); });

View File

@ -0,0 +1,158 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
var DEFAULT_BRACKETS = "()[]{}''\"\"";
var DEFAULT_EXPLODE_ON_ENTER = "[]{}";
var SPACE_CHAR_REGEX = /\s/;
var Pos = CodeMirror.Pos;
CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
if (old != CodeMirror.Init && old)
cm.removeKeyMap("autoCloseBrackets");
if (!val) return;
var pairs = DEFAULT_BRACKETS, explode = DEFAULT_EXPLODE_ON_ENTER;
if (typeof val == "string") pairs = val;
else if (typeof val == "object") {
if (val.pairs != null) pairs = val.pairs;
if (val.explode != null) explode = val.explode;
}
var map = buildKeymap(pairs);
if (explode) map.Enter = buildExplodeHandler(explode);
cm.addKeyMap(map);
});
function charsAround(cm, pos) {
var str = cm.getRange(Pos(pos.line, pos.ch - 1),
Pos(pos.line, pos.ch + 1));
return str.length == 2 ? str : null;
}
// Project the token type that will exists after the given char is
// typed, and use it to determine whether it would cause the start
// of a string token.
function enteringString(cm, pos, ch) {
var line = cm.getLine(pos.line);
var token = cm.getTokenAt(pos);
if (/\bstring2?\b/.test(token.type)) return false;
var stream = new CodeMirror.StringStream(line.slice(0, pos.ch) + ch + line.slice(pos.ch), 4);
stream.pos = stream.start = token.start;
for (;;) {
var type1 = cm.getMode().token(stream, token.state);
if (stream.pos >= pos.ch + 1) return /\bstring2?\b/.test(type1);
stream.start = stream.pos;
}
}
function buildKeymap(pairs) {
var map = {
name : "autoCloseBrackets",
Backspace: function(cm) {
if (cm.getOption("disableInput")) return CodeMirror.Pass;
var ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
if (!ranges[i].empty()) return CodeMirror.Pass;
var around = charsAround(cm, ranges[i].head);
if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
}
for (var i = ranges.length - 1; i >= 0; i--) {
var cur = ranges[i].head;
cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1));
}
}
};
var closingBrackets = "";
for (var i = 0; i < pairs.length; i += 2) (function(left, right) {
if (left != right) closingBrackets += right;
map["'" + left + "'"] = function(cm) {
if (cm.getOption("disableInput")) return CodeMirror.Pass;
var ranges = cm.listSelections(), type, next;
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i], cur = range.head, curType;
var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
if (!range.empty())
curType = "surround";
else if (left == right && next == right) {
if (cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == left + left + left)
curType = "skipThree";
else
curType = "skip";
} else if (left == right && cur.ch > 1 &&
cm.getRange(Pos(cur.line, cur.ch - 2), cur) == left + left &&
(cur.ch <= 2 || cm.getRange(Pos(cur.line, cur.ch - 3), Pos(cur.line, cur.ch - 2)) != left))
curType = "addFour";
else if (left == '"' || left == "'") {
if (!CodeMirror.isWordChar(next) && enteringString(cm, cur, left)) curType = "both";
else return CodeMirror.Pass;
} else if (cm.getLine(cur.line).length == cur.ch || closingBrackets.indexOf(next) >= 0 || SPACE_CHAR_REGEX.test(next))
curType = "both";
else
return CodeMirror.Pass;
if (!type) type = curType;
else if (type != curType) return CodeMirror.Pass;
}
cm.operation(function() {
if (type == "skip") {
cm.execCommand("goCharRight");
} else if (type == "skipThree") {
for (var i = 0; i < 3; i++)
cm.execCommand("goCharRight");
} else if (type == "surround") {
var sels = cm.getSelections();
for (var i = 0; i < sels.length; i++)
sels[i] = left + sels[i] + right;
cm.replaceSelections(sels, "around");
} else if (type == "both") {
cm.replaceSelection(left + right, null);
cm.execCommand("goCharLeft");
} else if (type == "addFour") {
cm.replaceSelection(left + left + left + left, "before");
cm.execCommand("goCharRight");
}
});
};
if (left != right) map["'" + right + "'"] = function(cm) {
var ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i];
if (!range.empty() ||
cm.getRange(range.head, Pos(range.head.line, range.head.ch + 1)) != right)
return CodeMirror.Pass;
}
cm.execCommand("goCharRight");
};
})(pairs.charAt(i), pairs.charAt(i + 1));
return map;
}
function buildExplodeHandler(pairs) {
return function(cm) {
if (cm.getOption("disableInput")) return CodeMirror.Pass;
var ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
if (!ranges[i].empty()) return CodeMirror.Pass;
var around = charsAround(cm, ranges[i].head);
if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
}
cm.operation(function() {
cm.replaceSelection("\n\n", null);
cm.execCommand("goCharLeft");
ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
var line = ranges[i].head.line;
cm.indentLine(line, null, true);
cm.indentLine(line + 1, null, true);
}
});
};
}
});

View File

@ -94,33 +94,19 @@
} }
} }
function autoCloseCurrent(cm, typingSlash) { function autoCloseSlash(cm) {
if (cm.getOption("disableInput")) return CodeMirror.Pass;
var ranges = cm.listSelections(), replacements = []; var ranges = cm.listSelections(), replacements = [];
var head = typingSlash ? "/" : "</";
for (var i = 0; i < ranges.length; i++) { for (var i = 0; i < ranges.length; i++) {
if (!ranges[i].empty()) return CodeMirror.Pass; if (!ranges[i].empty()) return CodeMirror.Pass;
var pos = ranges[i].head, tok = cm.getTokenAt(pos); var pos = ranges[i].head, tok = cm.getTokenAt(pos);
var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state; var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
if (typingSlash && (tok.type == "string" || tok.string.charAt(0) != "<" || if (tok.type == "string" || tok.string.charAt(0) != "<" ||
tok.start != pos.ch - 1)) tok.start != pos.ch - 1 || inner.mode.name != "xml" ||
!state.context || !state.context.tagName ||
closingTagExists(cm, state.context.tagName, pos, state))
return CodeMirror.Pass; return CodeMirror.Pass;
// Kludge to get around the fact that we are not in XML mode replacements[i] = "/" + state.context.tagName + ">";
// when completing in JS/CSS snippet in htmlmixed mode. Does not
// work for other XML embedded languages (there is no general
// way to go from a mixed mode to its current XML state).
if (inner.mode.name != "xml") {
if (cm.getMode().name == "htmlmixed" && inner.mode.name == "javascript")
replacements[i] = head + "script>";
else if (cm.getMode().name == "htmlmixed" && inner.mode.name == "css")
replacements[i] = head + "style>";
else
return CodeMirror.Pass;
} else {
if (!state.context || !state.context.tagName ||
closingTagExists(cm, state.context.tagName, pos, state))
return CodeMirror.Pass;
replacements[i] = head + state.context.tagName + ">";
}
} }
cm.replaceSelections(replacements); cm.replaceSelections(replacements);
ranges = cm.listSelections(); ranges = cm.listSelections();
@ -129,13 +115,6 @@
cm.indentLine(ranges[i].head.line); cm.indentLine(ranges[i].head.line);
} }
function autoCloseSlash(cm) {
if (cm.getOption("disableInput")) return CodeMirror.Pass;
return autoCloseCurrent(cm, true);
}
CodeMirror.commands.closeTag = function(cm) { return autoCloseCurrent(cm); };
function indexOf(collection, elt) { function indexOf(collection, elt) {
if (collection.indexOf) return collection.indexOf(elt); if (collection.indexOf) return collection.indexOf(elt);
for (var i = 0, e = collection.length; i < e; ++i) for (var i = 0, e = collection.length; i < e; ++i)

View File

@ -0,0 +1,38 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
var listRE = /^(\s*)([*+-]|(\d+)\.)(\s+)/,
unorderedBullets = "*+-";
CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) {
if (cm.getOption("disableInput")) return CodeMirror.Pass;
var ranges = cm.listSelections(), replacements = [];
for (var i = 0; i < ranges.length; i++) {
var pos = ranges[i].head, match;
var inList = cm.getStateAfter(pos.line).list !== false;
if (!ranges[i].empty() || !inList || !(match = cm.getLine(pos.line).match(listRE))) {
cm.execCommand("newlineAndIndent");
return;
}
var indent = match[1], after = match[4];
var bullet = unorderedBullets.indexOf(match[2]) >= 0
? match[2]
: (parseInt(match[3], 10) + 1) + ".";
replacements[i] = "\n" + indent + bullet + after;
}
cm.replaceSelections(replacements);
};
});

View File

@ -81,7 +81,7 @@
if (marks.length) { if (marks.length) {
// Kludge to work around the IE bug from issue #1193, where text // Kludge to work around the IE bug from issue #1193, where text
// input stops going to the textare whever this fires. // input stops going to the textare whever this fires.
if (ie_lt8 && cm.state.focused) cm.focus(); if (ie_lt8 && cm.state.focused) cm.display.input.focus();
var clear = function() { var clear = function() {
cm.operation(function() { cm.operation(function() {

Some files were not shown because too many files have changed in this diff Show More